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
.HashSet
;
57 import java
.util
.Iterator
;
58 import java
.util
.List
;
62 import org
.spearce
.jgit
.util
.FS
;
65 * An object representing the Git config file.
67 * This can be either the repository specific file or the user global
68 * file depending on how it is instantiated.
70 public class RepositoryConfig
{
72 * Obtain a new configuration instance for ~/.gitconfig.
74 * @return a new configuration instance to read the user's global
75 * configuration file from their home directory.
77 public static RepositoryConfig
openUserConfig() {
78 return new RepositoryConfig(null, new File(FS
.userHome(), ".gitconfig"));
81 private final RepositoryConfig baseConfig
;
83 /** Section name for a remote configuration */
84 public static final String REMOTE_SECTION
= "remote";
86 /** Section name for a branch configuration. */
87 public static final String BRANCH_SECTION
= "branch";
89 private final File configFile
;
91 private boolean readFile
;
93 private CoreConfig core
;
95 private List
<Entry
> entries
;
97 private Map
<String
, Object
> byName
;
99 private static final String MAGIC_EMPTY_VALUE
= "%%magic%%empty%%";
101 RepositoryConfig(final Repository repo
) {
102 this(openUserConfig(), FS
.resolve(repo
.getDirectory(), "config"));
106 * Create a Git configuration file reader/writer/cache for a specific file.
109 * configuration that provides default values if this file does
110 * not set/override a particular key. Often this is the user's
111 * global configuration file, or the system level configuration.
113 * path of the file to load (or save).
115 public RepositoryConfig(final RepositoryConfig base
, final File cfgLocation
) {
117 configFile
= cfgLocation
;
122 * @return Core configuration values
124 public CoreConfig
getCore() {
129 * Obtain an integer value from the configuration.
132 * section the key is grouped within.
134 * name of the key to get.
135 * @param defaultValue
136 * default value to return if no value was present.
137 * @return an integer value from the configuration, or defaultValue.
139 public int getInt(final String section
, final String name
,
140 final int defaultValue
) {
141 return getInt(section
, null, name
, defaultValue
);
145 * Obtain an integer value from the configuration.
148 * section the key is grouped within.
150 * subsection name, such a remote or branch name.
152 * name of the key to get.
153 * @param defaultValue
154 * default value to return if no value was present.
155 * @return an integer value from the configuration, or defaultValue.
157 public int getInt(final String section
, String subsection
,
158 final String name
, final int defaultValue
) {
159 final String str
= getString(section
, subsection
, name
);
163 String n
= str
.trim();
168 switch (Character
.toLowerCase(n
.charAt(n
.length() - 1))) {
170 mul
= 1024 * 1024 * 1024;
180 n
= n
.substring(0, n
.length() - 1).trim();
185 return mul
* Integer
.parseInt(n
);
186 } catch (NumberFormatException nfe
) {
187 throw new IllegalArgumentException("Invalid integer value: "
188 + section
+ "." + name
+ "=" + str
);
193 * Get a boolean value from the git config
196 * section the key is grouped within.
198 * name of the key to get.
199 * @param defaultValue
200 * default value to return if no value was present.
201 * @return true if any value or defaultValue is true, false for missing or
204 protected boolean getBoolean(final String section
, final String name
,
205 final boolean defaultValue
) {
206 return getBoolean(section
, null, name
, defaultValue
);
210 * Get a boolean value from the git config
213 * section the key is grouped within.
215 * subsection name, such a remote or branch name.
217 * name of the key to get.
218 * @param defaultValue
219 * default value to return if no value was present.
220 * @return true if any value or defaultValue is true, false for missing or
223 protected boolean getBoolean(final String section
, String subsection
,
224 final String name
, final boolean defaultValue
) {
225 String n
= getRawString(section
, subsection
, name
);
230 if (MAGIC_EMPTY_VALUE
.equals(n
) || "yes".equals(n
) || "true".equals(n
) || "1".equals(n
)) {
232 } else if ("no".equals(n
) || "false".equals(n
) || "0".equals(n
)) {
235 throw new IllegalArgumentException("Invalid boolean value: "
236 + section
+ "." + name
+ "=" + n
);
244 * @return a String value from git config.
246 public String
getString(final String section
, String subsection
, final String name
) {
247 String val
= getRawString(section
, subsection
, name
);
248 if (MAGIC_EMPTY_VALUE
.equals(val
)) {
258 * @return array of zero or more values from the configuration.
260 public String
[] getStringList(final String section
, String subsection
,
262 final Object o
= getRawEntry(section
, subsection
, name
);
263 if (o
instanceof List
) {
264 final List lst
= (List
) o
;
265 final String
[] r
= new String
[lst
.size()];
266 for (int i
= 0; i
< r
.length
; i
++) {
267 final String val
= ((Entry
) lst
.get(i
)).value
;
268 r
[i
] = MAGIC_EMPTY_VALUE
.equals(val
) ?
"" : val
;
273 if (o
instanceof Entry
) {
274 final String val
= ((Entry
) o
).value
;
275 return new String
[] { MAGIC_EMPTY_VALUE
.equals(val
) ?
"" : val
};
278 if (baseConfig
!= null)
279 return baseConfig
.getStringList(section
, subsection
, name
);
280 return new String
[0];
285 * section to search for.
286 * @return set of all subsections of specified section within this
287 * configuration and its base configuration; may be empty if no
290 public Set
<String
> getSubsections(final String section
) {
291 final Set
<String
> result
= new HashSet
<String
>();
293 for (final Entry e
: entries
) {
294 if (section
.equals(e
.base
) && e
.extendedBase
!= null)
295 result
.add(e
.extendedBase
);
297 if (baseConfig
!= null)
298 result
.addAll(baseConfig
.getSubsections(section
));
302 private String
getRawString(final String section
, final String subsection
,
304 final Object o
= getRawEntry(section
, subsection
, name
);
305 if (o
instanceof List
) {
306 return ((Entry
) ((List
) o
).get(0)).value
;
307 } else if (o
instanceof Entry
) {
308 return ((Entry
) o
).value
;
309 } else if (baseConfig
!= null)
310 return baseConfig
.getRawString(section
, subsection
, name
);
315 private Object
getRawEntry(final String section
, final String subsection
,
320 } catch (FileNotFoundException err
) {
321 // Oh well. No sense in complaining about it.
323 } catch (IOException err
) {
324 err
.printStackTrace();
329 if (subsection
!= null)
330 ss
= "."+subsection
.toLowerCase();
334 o
= byName
.get(section
.toLowerCase() + ss
+ "." + name
.toLowerCase());
339 * Add or modify a configuration value. The parameters will result in a
340 * configuration entry like this.
343 * [section "subsection"]
348 * section name, e.g "branch"
350 * optional subsection value, e.g. a branch name
352 * parameter name, e.g. "filemode"
354 * parameter value, e.g. "true"
356 public void setString(final String section
, final String subsection
,
357 final String name
, final String value
) {
358 setStringList(section
, subsection
, name
, Collections
359 .singletonList(value
));
363 * Remove a configuration value.
366 * section name, e.g "branch"
368 * optional subsection value, e.g. a branch name
370 * parameter name, e.g. "filemode"
372 public void unsetString(final String section
, final String subsection
,
374 setStringList(section
, subsection
, name
, Collections
375 .<String
> emptyList());
379 * Set a configuration value.
382 * [section "subsection"]
387 * section name, e.g "branch"
389 * optional subsection value, e.g. a branch name
391 * parameter name, e.g. "filemode"
393 * list of zero or more values for this key.
395 public void setStringList(final String section
, final String subsection
,
396 final String name
, final List
<String
> values
) {
397 // Update our parsed cache of values for future reference.
399 String key
= section
.toLowerCase();
400 if (subsection
!= null)
401 key
+= "." + subsection
.toLowerCase();
402 key
+= "." + name
.toLowerCase();
403 if (values
.size() == 0)
405 else if (values
.size() == 1) {
406 final Entry e
= new Entry();
408 e
.extendedBase
= subsection
;
410 e
.value
= values
.get(0);
413 final ArrayList
<Entry
> eList
= new ArrayList
<Entry
>(values
.size());
414 for (final String v
: values
) {
415 final Entry e
= new Entry();
417 e
.extendedBase
= subsection
;
422 byName
.put(key
, eList
);
427 int insertPosition
= -1;
429 // Reset the first n Entry objects that match this input name.
431 while (entryIndex
< entries
.size() && valueIndex
< values
.size()) {
432 final Entry e
= entries
.get(entryIndex
++);
433 if (e
.match(section
, subsection
, name
)) {
434 e
.value
= values
.get(valueIndex
++);
435 insertPosition
= entryIndex
;
439 // Remove any extra Entry objects that we no longer need.
441 if (valueIndex
== values
.size() && entryIndex
< entries
.size()) {
442 while (entryIndex
< entries
.size()) {
443 final Entry e
= entries
.get(entryIndex
++);
444 if (e
.match(section
, subsection
, name
))
445 entries
.remove(--entryIndex
);
449 // Insert new Entry objects for additional/new values.
451 if (valueIndex
< values
.size() && entryIndex
== entries
.size()){
452 if (insertPosition
< 0) {
453 // We didn't find a matching key above, but maybe there
454 // is already a section available that matches. Insert
455 // after the last key of that section.
457 insertPosition
= findSectionEnd(section
, subsection
);
459 if (insertPosition
< 0) {
460 // We didn't find any matching section header for this key,
461 // so we must create a new section header at the end.
463 final Entry e
= new Entry();
467 e
.extendedBase
= subsection
;
469 insertPosition
= entries
.size();
471 while (valueIndex
< values
.size()) {
472 final Entry e
= new Entry();
476 e
.extendedBase
= subsection
;
478 e
.value
= values
.get(valueIndex
++);
479 entries
.add(insertPosition
++, e
);
484 private int findSectionEnd(final String section
, final String subsection
) {
485 for (int i
= 0; i
< entries
.size(); i
++) {
486 Entry e
= entries
.get(i
);
487 if (e
.match(section
, subsection
, null)) {
489 while (i
< entries
.size()) {
491 if (e
.match(section
, subsection
, e
.name
))
503 * Create a new default config
505 public void create() {
517 e
.name
= "repositoryformatversion";
527 core
= new CoreConfig(this);
531 * Save config data to the git config file
533 * @throws IOException
535 public void save() throws IOException
{
536 final File tmp
= new File(configFile
.getParentFile(), configFile
539 final PrintWriter r
= new PrintWriter(new BufferedWriter(
540 new OutputStreamWriter(new FileOutputStream(tmp
),
541 Constants
.CHARSET
))) {
543 public void println() {
549 final Iterator
<Entry
> i
= entries
.iterator();
550 while (i
.hasNext()) {
551 final Entry e
= i
.next();
552 if (e
.prefix
!= null) {
555 if (e
.base
!= null && e
.name
== null) {
558 if (e
.extendedBase
!= null) {
561 r
.print(escapeValue(e
.extendedBase
));
565 } else if (e
.base
!= null && e
.name
!= null) {
566 if (e
.prefix
== null || "".equals(e
.prefix
)) {
570 if (e
.value
!= null) {
571 if (!MAGIC_EMPTY_VALUE
.equals(e
.value
)) {
573 r
.print(escapeValue(e
.value
));
576 if (e
.suffix
!= null) {
580 if (e
.suffix
!= null) {
587 if (!tmp
.renameTo(configFile
)) {
589 if (!tmp
.renameTo(configFile
))
590 throw new IOException("Cannot save config file " + configFile
+ ", rename failed");
594 if (tmp
.exists() && !tmp
.delete()) {
595 System
.err
.println("(warning) failed to delete tmp config file: " + tmp
);
602 * Read the config file
603 * @throws IOException
605 public void load() throws IOException
{
608 final BufferedReader r
= new BufferedReader(new InputStreamReader(
609 new FileInputStream(configFile
), Constants
.CHARSET
));
612 Entry e
= new Entry();
615 int input
= r
.read();
616 final char in
= (char) input
;
619 } else if ('\n' == in
) {
620 // End of this entry.
622 if (e
.base
!= null) {
626 } else if (e
.suffix
!= null) {
627 // Everything up until the end-of-line is in the suffix.
629 } else if (';' == in
|| '#' == in
) {
630 // The rest of this line is a comment; put into suffix.
631 e
.suffix
= String
.valueOf(in
);
632 } else if (e
.base
== null && Character
.isWhitespace(in
)) {
633 // Save the leading whitespace (if any).
634 if (e
.prefix
== null) {
638 } else if ('[' == in
) {
639 // This is a group header line.
640 e
.base
= readBase(r
);
643 e
.extendedBase
= readValue(r
, true, '"');
647 throw new IOException("Bad group header.");
650 } else if (last
!= null) {
653 e
.extendedBase
= last
.extendedBase
;
655 e
.name
= readName(r
);
656 if (e
.name
.endsWith("\n")) {
657 e
.name
= e
.name
.substring(0, e
.name
.length()-1);
658 e
.value
= MAGIC_EMPTY_VALUE
;
660 e
.value
= readValue(r
, false, -1);
662 throw new IOException("Invalid line in config file.");
669 core
= new CoreConfig(this);
672 private void clear() {
673 entries
= new ArrayList
<Entry
>();
674 byName
= new HashMap
<String
, Object
>();
677 @SuppressWarnings("unchecked")
678 private void add(final Entry e
) {
680 if (e
.base
!= null) {
681 final String b
= e
.base
.toLowerCase();
683 if (e
.extendedBase
!= null) {
684 group
= b
+ "." + e
.extendedBase
;
688 if (e
.name
!= null) {
689 final String n
= e
.name
.toLowerCase();
690 final String key
= group
+ "." + n
;
691 final Object o
= byName
.get(key
);
694 } else if (o
instanceof Entry
) {
695 final ArrayList
<Object
> l
= new ArrayList
<Object
>();
699 } else if (o
instanceof List
) {
700 ((List
<Entry
>) o
).add(e
);
706 private static String
escapeValue(final String x
) {
707 boolean inquote
= false;
709 final StringBuffer r
= new StringBuffer(x
.length());
710 for (int k
= 0; k
< x
.length(); k
++) {
711 final char c
= x
.charAt(k
);
719 lineStart
= r
.length();
741 r
.insert(lineStart
, '"');
748 if (!inquote
&& r
.length() > 0
749 && r
.charAt(r
.length() - 1) == ' ') {
750 r
.insert(lineStart
, '"');
767 private static String
readBase(final BufferedReader r
) throws IOException
{
768 final StringBuffer base
= new StringBuffer();
773 throw new IOException("Unexpected end of config file.");
774 } else if (']' == c
) {
777 } else if (' ' == c
|| '\t' == c
) {
782 throw new IOException("Unexpected end of config file.");
783 } else if ('"' == c
) {
786 } else if (' ' == c
|| '\t' == c
) {
789 throw new IOException("Bad base entry. : " + base
+ "," + c
);
793 } else if (Character
.isLetterOrDigit((char) c
) || '.' == c
|| '-' == c
) {
794 base
.append((char) c
);
796 throw new IOException("Bad base entry. : " + base
+ ", " + c
);
799 return base
.toString();
802 private static String
readName(final BufferedReader r
) throws IOException
{
803 final StringBuffer name
= new StringBuffer();
808 throw new IOException("Unexpected end of config file.");
809 } else if ('=' == c
) {
811 } else if (' ' == c
|| '\t' == c
) {
816 throw new IOException("Unexpected end of config file.");
817 } else if ('=' == c
) {
819 } else if (';' == c
|| '#' == c
|| '\n' == c
) {
822 } else if (' ' == c
|| '\t' == c
) {
825 throw new IOException("Bad entry delimiter.");
829 } else if (Character
.isLetterOrDigit((char) c
) || c
== '-') {
830 // From the git-config man page:
831 // The variable names are case-insensitive and only
832 // alphanumeric characters and - are allowed.
833 name
.append((char) c
);
834 } else if ('\n' == c
) {
836 name
.append((char) c
);
839 throw new IOException("Bad config entry name: " + name
+ (char) c
);
842 return name
.toString();
845 private static String
readValue(final BufferedReader r
, boolean quote
,
846 final int eol
) throws IOException
{
847 final StringBuffer value
= new StringBuffer();
848 boolean space
= false;
853 if (value
.length() == 0)
854 throw new IOException("Unexpected end of config file.");
859 throw new IOException("Newline in quotes not allowed.");
868 if (Character
.isWhitespace((char) c
)) {
872 if (';' == c
|| '#' == c
) {
878 if (value
.length() > 0) {
887 throw new IOException("End of file in escape.");
906 throw new IOException("Bad escape: " + ((char) c
));
913 value
.append((char) c
);
915 return value
.length() > 0 ? value
.toString() : null;
918 public String
toString() {
919 return "RepositoryConfig[" + configFile
.getPath() + "]";
935 boolean match(final String aBase
, final String aExtendedBase
,
936 final String aName
) {
937 return eq(base
, aBase
) && eq(extendedBase
, aExtendedBase
)
941 private static boolean eq(final String a
, final String b
) {
944 if (a
== null || b
== null)