Fix RemoteRefUpdate to delete local tracking ref upon successful deletion
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RepositoryConfig.java
blobd3c29ac978d41c77c9c40e408fd4ede4a09492c5
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.HashSet;
57 import java.util.Iterator;
58 import java.util.List;
59 import java.util.Map;
60 import java.util.Set;
62 import org.spearce.jgit.util.FS;
64 /**
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 {
71 /**
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.
108 * @param base
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.
112 * @param cfgLocation
113 * path of the file to load (or save).
115 public RepositoryConfig(final RepositoryConfig base, final File cfgLocation) {
116 baseConfig = base;
117 configFile = cfgLocation;
118 clear();
122 * @return Core configuration values
124 public CoreConfig getCore() {
125 return core;
129 * Obtain an integer value from the configuration.
131 * @param section
132 * section the key is grouped within.
133 * @param name
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.
147 * @param section
148 * section the key is grouped within.
149 * @param subsection
150 * subsection name, such a remote or branch name.
151 * @param 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);
160 if (str == null)
161 return defaultValue;
163 String n = str.trim();
164 if (n.length() == 0)
165 return defaultValue;
167 int mul = 1;
168 switch (Character.toLowerCase(n.charAt(n.length() - 1))) {
169 case 'g':
170 mul = 1024 * 1024 * 1024;
171 break;
172 case 'm':
173 mul = 1024 * 1024;
174 break;
175 case 'k':
176 mul = 1024;
177 break;
179 if (mul > 1)
180 n = n.substring(0, n.length() - 1).trim();
181 if (n.length() == 0)
182 return defaultValue;
184 try {
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
195 * @param section
196 * section the key is grouped within.
197 * @param name
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
202 * explicit false
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
212 * @param section
213 * section the key is grouped within.
214 * @param subsection
215 * subsection name, such a remote or branch name.
216 * @param 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
221 * explicit false
223 protected boolean getBoolean(final String section, String subsection,
224 final String name, final boolean defaultValue) {
225 String n = getRawString(section, subsection, name);
226 if (n == null)
227 return defaultValue;
229 n = n.toLowerCase();
230 if (MAGIC_EMPTY_VALUE.equals(n) || "yes".equals(n) || "true".equals(n) || "1".equals(n)) {
231 return true;
232 } else if ("no".equals(n) || "false".equals(n) || "0".equals(n)) {
233 return false;
234 } else {
235 throw new IllegalArgumentException("Invalid boolean value: "
236 + section + "." + name + "=" + n);
241 * @param section
242 * @param subsection
243 * @param name
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)) {
249 return "";
251 return val;
255 * @param section
256 * @param subsection
257 * @param name
258 * @return array of zero or more values from the configuration.
260 public String[] getStringList(final String section, String subsection,
261 final String name) {
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;
270 return r;
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];
284 * @param section
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
288 * subsection exists.
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));
299 return result;
302 private String getRawString(final String section, final String subsection,
303 final String name) {
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);
311 else
312 return null;
315 private Object getRawEntry(final String section, final String subsection,
316 final String name) {
317 if (!readFile) {
318 try {
319 load();
320 } catch (FileNotFoundException err) {
321 // Oh well. No sense in complaining about it.
323 } catch (IOException err) {
324 err.printStackTrace();
328 String ss;
329 if (subsection != null)
330 ss = "."+subsection.toLowerCase();
331 else
332 ss = "";
333 final Object o;
334 o = byName.get(section.toLowerCase() + ss + "." + name.toLowerCase());
335 return o;
339 * Add or modify a configuration value. The parameters will result in a
340 * configuration entry like this.
342 * <pre>
343 * [section &quot;subsection&quot;]
344 * name = value
345 * </pre>
347 * @param section
348 * section name, e.g "branch"
349 * @param subsection
350 * optional subsection value, e.g. a branch name
351 * @param name
352 * parameter name, e.g. "filemode"
353 * @param value
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.
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"
372 public void unsetString(final String section, final String subsection,
373 final String name) {
374 setStringList(section, subsection, name, Collections
375 .<String> emptyList());
379 * Set a configuration value.
381 * <pre>
382 * [section &quot;subsection&quot;]
383 * name = value
384 * </pre>
386 * @param section
387 * section name, e.g "branch"
388 * @param subsection
389 * optional subsection value, e.g. a branch name
390 * @param name
391 * parameter name, e.g. "filemode"
392 * @param values
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)
404 byName.remove(key);
405 else if (values.size() == 1) {
406 final Entry e = new Entry();
407 e.base = section;
408 e.extendedBase = subsection;
409 e.name = name;
410 e.value = values.get(0);
411 byName.put(key, e);
412 } else {
413 final ArrayList<Entry> eList = new ArrayList<Entry>(values.size());
414 for (final String v : values) {
415 final Entry e = new Entry();
416 e.base = section;
417 e.extendedBase = subsection;
418 e.name = name;
419 e.value = v;
420 eList.add(e);
422 byName.put(key, eList);
425 int entryIndex = 0;
426 int valueIndex = 0;
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();
464 e.prefix = null;
465 e.suffix = null;
466 e.base = section;
467 e.extendedBase = subsection;
468 entries.add(e);
469 insertPosition = entries.size();
471 while (valueIndex < values.size()) {
472 final Entry e = new Entry();
473 e.prefix = null;
474 e.suffix = null;
475 e.base = section;
476 e.extendedBase = subsection;
477 e.name = name;
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)) {
488 i++;
489 while (i < entries.size()) {
490 e = entries.get(i);
491 if (e.match(section, subsection, e.name))
492 i++;
493 else
494 break;
496 return i;
499 return -1;
503 * Create a new default config
505 public void create() {
506 Entry e;
508 clear();
509 readFile = true;
511 e = new Entry();
512 e.base = "core";
513 add(e);
515 e = new Entry();
516 e.base = "core";
517 e.name = "repositoryformatversion";
518 e.value = "0";
519 add(e);
521 e = new Entry();
522 e.base = "core";
523 e.name = "filemode";
524 e.value = "true";
525 add(e);
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
537 .getName()
538 + ".lock");
539 final PrintWriter r = new PrintWriter(new BufferedWriter(
540 new OutputStreamWriter(new FileOutputStream(tmp),
541 Constants.CHARSET))) {
542 @Override
543 public void println() {
544 print('\n');
547 boolean ok = false;
548 try {
549 final Iterator<Entry> i = entries.iterator();
550 while (i.hasNext()) {
551 final Entry e = i.next();
552 if (e.prefix != null) {
553 r.print(e.prefix);
555 if (e.base != null && e.name == null) {
556 r.print('[');
557 r.print(e.base);
558 if (e.extendedBase != null) {
559 r.print(' ');
560 r.print('"');
561 r.print(escapeValue(e.extendedBase));
562 r.print('"');
564 r.print(']');
565 } else if (e.base != null && e.name != null) {
566 if (e.prefix == null || "".equals(e.prefix)) {
567 r.print('\t');
569 r.print(e.name);
570 if (e.value != null) {
571 if (!MAGIC_EMPTY_VALUE.equals(e.value)) {
572 r.print(" = ");
573 r.print(escapeValue(e.value));
576 if (e.suffix != null) {
577 r.print(' ');
580 if (e.suffix != null) {
581 r.print(e.suffix);
583 r.println();
585 ok = true;
586 r.close();
587 if (!tmp.renameTo(configFile)) {
588 configFile.delete();
589 if (!tmp.renameTo(configFile))
590 throw new IOException("Cannot save config file " + configFile + ", rename failed");
592 } finally {
593 r.close();
594 if (tmp.exists() && !tmp.delete()) {
595 System.err.println("(warning) failed to delete tmp config file: " + tmp);
598 readFile = ok;
602 * Read the config file
603 * @throws IOException
605 public void load() throws IOException {
606 clear();
607 readFile = true;
608 final BufferedReader r = new BufferedReader(new InputStreamReader(
609 new FileInputStream(configFile), Constants.CHARSET));
610 try {
611 Entry last = null;
612 Entry e = new Entry();
613 for (;;) {
614 r.mark(1);
615 int input = r.read();
616 final char in = (char) input;
617 if (-1 == input) {
618 break;
619 } else if ('\n' == in) {
620 // End of this entry.
621 add(e);
622 if (e.base != null) {
623 last = e;
625 e = new Entry();
626 } else if (e.suffix != null) {
627 // Everything up until the end-of-line is in the suffix.
628 e.suffix += in;
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) {
635 e.prefix = "";
637 e.prefix += in;
638 } else if ('[' == in) {
639 // This is a group header line.
640 e.base = readBase(r);
641 input = r.read();
642 if ('"' == input) {
643 e.extendedBase = readValue(r, true, '"');
644 input = r.read();
646 if (']' != input) {
647 throw new IOException("Bad group header.");
649 e.suffix = "";
650 } else if (last != null) {
651 // Read a value.
652 e.base = last.base;
653 e.extendedBase = last.extendedBase;
654 r.reset();
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;
659 } else
660 e.value = readValue(r, false, -1);
661 } else {
662 throw new IOException("Invalid line in config file.");
665 } finally {
666 r.close();
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) {
679 entries.add(e);
680 if (e.base != null) {
681 final String b = e.base.toLowerCase();
682 final String group;
683 if (e.extendedBase != null) {
684 group = b + "." + e.extendedBase;
685 } else {
686 group = b;
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);
692 if (o == null) {
693 byName.put(key, e);
694 } else if (o instanceof Entry) {
695 final ArrayList<Object> l = new ArrayList<Object>();
696 l.add(o);
697 l.add(e);
698 byName.put(key, l);
699 } else if (o instanceof List) {
700 ((List<Entry>) o).add(e);
706 private static String escapeValue(final String x) {
707 boolean inquote = false;
708 int lineStart = 0;
709 final StringBuffer r = new StringBuffer(x.length());
710 for (int k = 0; k < x.length(); k++) {
711 final char c = x.charAt(k);
712 switch (c) {
713 case '\n':
714 if (inquote) {
715 r.append('"');
716 inquote = false;
718 r.append("\\n\\\n");
719 lineStart = r.length();
720 break;
722 case '\t':
723 r.append("\\t");
724 break;
726 case '\b':
727 r.append("\\b");
728 break;
730 case '\\':
731 r.append("\\\\");
732 break;
734 case '"':
735 r.append("\\\"");
736 break;
738 case ';':
739 case '#':
740 if (!inquote) {
741 r.insert(lineStart, '"');
742 inquote = true;
744 r.append(c);
745 break;
747 case ' ':
748 if (!inquote && r.length() > 0
749 && r.charAt(r.length() - 1) == ' ') {
750 r.insert(lineStart, '"');
751 inquote = true;
753 r.append(' ');
754 break;
756 default:
757 r.append(c);
758 break;
761 if (inquote) {
762 r.append('"');
764 return r.toString();
767 private static String readBase(final BufferedReader r) throws IOException {
768 final StringBuffer base = new StringBuffer();
769 for (;;) {
770 r.mark(1);
771 int c = r.read();
772 if (c < 0) {
773 throw new IOException("Unexpected end of config file.");
774 } else if (']' == c) {
775 r.reset();
776 break;
777 } else if (' ' == c || '\t' == c) {
778 for (;;) {
779 r.mark(1);
780 c = r.read();
781 if (c < 0) {
782 throw new IOException("Unexpected end of config file.");
783 } else if ('"' == c) {
784 r.reset();
785 break;
786 } else if (' ' == c || '\t' == c) {
787 // Skipped...
788 } else {
789 throw new IOException("Bad base entry. : " + base + "," + c);
792 break;
793 } else if (Character.isLetterOrDigit((char) c) || '.' == c || '-' == c) {
794 base.append((char) c);
795 } else {
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();
804 for (;;) {
805 r.mark(1);
806 int c = r.read();
807 if (c < 0) {
808 throw new IOException("Unexpected end of config file.");
809 } else if ('=' == c) {
810 break;
811 } else if (' ' == c || '\t' == c) {
812 for (;;) {
813 r.mark(1);
814 c = r.read();
815 if (c < 0) {
816 throw new IOException("Unexpected end of config file.");
817 } else if ('=' == c) {
818 break;
819 } else if (';' == c || '#' == c || '\n' == c) {
820 r.reset();
821 break;
822 } else if (' ' == c || '\t' == c) {
823 // Skipped...
824 } else {
825 throw new IOException("Bad entry delimiter.");
828 break;
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) {
835 r.reset();
836 name.append((char) c);
837 break;
838 } else {
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;
849 for (;;) {
850 r.mark(1);
851 int c = r.read();
852 if (c < 0) {
853 if (value.length() == 0)
854 throw new IOException("Unexpected end of config file.");
855 break;
857 if ('\n' == c) {
858 if (quote) {
859 throw new IOException("Newline in quotes not allowed.");
861 r.reset();
862 break;
864 if (eol == c) {
865 break;
867 if (!quote) {
868 if (Character.isWhitespace((char) c)) {
869 space = true;
870 continue;
872 if (';' == c || '#' == c) {
873 r.reset();
874 break;
877 if (space) {
878 if (value.length() > 0) {
879 value.append(' ');
881 space = false;
883 if ('\\' == c) {
884 c = r.read();
885 switch (c) {
886 case -1:
887 throw new IOException("End of file in escape.");
888 case '\n':
889 continue;
890 case 't':
891 value.append('\t');
892 continue;
893 case 'b':
894 value.append('\b');
895 continue;
896 case 'n':
897 value.append('\n');
898 continue;
899 case '\\':
900 value.append('\\');
901 continue;
902 case '"':
903 value.append('"');
904 continue;
905 default:
906 throw new IOException("Bad escape: " + ((char) c));
909 if ('"' == c) {
910 quote = !quote;
911 continue;
913 value.append((char) c);
915 return value.length() > 0 ? value.toString() : null;
918 public String toString() {
919 return "RepositoryConfig[" + configFile.getPath() + "]";
922 static class Entry {
923 String prefix;
925 String base;
927 String extendedBase;
929 String name;
931 String value;
933 String suffix;
935 boolean match(final String aBase, final String aExtendedBase,
936 final String aName) {
937 return eq(base, aBase) && eq(extendedBase, aExtendedBase)
938 && eq(name, aName);
941 private static boolean eq(final String a, final String b) {
942 if (a == b)
943 return true;
944 if (a == null || b == null)
945 return false;
946 return a.equals(b);