Merge branch 'jc/attr'
authorJunio C Hamano <junkio@cox.net>
Sun, 22 Apr 2007 00:38:00 +0000 (17:38 -0700)
committerJunio C Hamano <junkio@cox.net>
Sun, 22 Apr 2007 00:38:00 +0000 (17:38 -0700)
* 'jc/attr': (28 commits)
  lockfile: record the primary process.
  convert.c: restructure the attribute checking part.
  Fix bogus linked-list management for user defined merge drivers.
  Simplify calling of CR/LF conversion routines
  Document gitattributes(5)
  Update 'crlf' attribute semantics.
  Documentation: support manual section (5) - file formats.
  Simplify code to find recursive merge driver.
  Counto-fix in merge-recursive
  Fix funny types used in attribute value representation
  Allow low-level driver to specify different behaviour during internal merge.
  Custom low-level merge driver: change the configuration scheme.
  Allow the default low-level merge driver to be configured.
  Custom low-level merge driver support.
  Add a demonstration/test of customized merge.
  Allow specifying specialized merge-backend per path.
  merge-recursive: separate out xdl_merge() interface.
  Allow more than true/false to attributes.
  Document git-check-attr
  Change attribute negation marker from '!' to '-'.
  ...

22 files changed:
.gitignore
Documentation/Makefile
Documentation/cmd-list.perl
Documentation/config.txt
Documentation/git-check-attr.txt [new file with mode: 0644]
Documentation/gitattributes.txt [new file with mode: 0644]
Makefile
attr.c [new file with mode: 0644]
attr.h [new file with mode: 0644]
builtin-apply.c
builtin-check-attr.c [new file with mode: 0644]
builtin.h
cache.h
convert.c
diff.c
entry.c
git.c
lockfile.c
merge-recursive.c
sha1_file.c
t/t0020-crlf.sh
t/t6026-merge-attr.sh [new file with mode: 0755]

index fa7ac93..4dc0c39 100644 (file)
@@ -16,6 +16,7 @@ git-blame
 git-branch
 git-bundle
 git-cat-file
+git-check-attr
 git-check-ref-format
 git-checkout
 git-checkout-index
index a637d8d..8d3617d 100644 (file)
@@ -2,9 +2,10 @@ MAN1_TXT= \
        $(filter-out $(addsuffix .txt, $(ARTICLES) $(SP_ARTICLES)), \
                $(wildcard git-*.txt)) \
        gitk.txt
+MAN5_TXT=gitattributes.txt
 MAN7_TXT=git.txt
 
-DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN7_TXT))
+DOC_HTML=$(patsubst %.txt,%.html,$(MAN1_TXT) $(MAN5_TXT) $(MAN7_TXT))
 
 ARTICLES = tutorial
 ARTICLES += tutorial-2
@@ -23,12 +24,14 @@ SP_ARTICLES = howto/revert-branch-rebase user-manual
 DOC_HTML += $(patsubst %,%.html,$(ARTICLES) $(SP_ARTICLES))
 
 DOC_MAN1=$(patsubst %.txt,%.1,$(MAN1_TXT))
+DOC_MAN5=$(patsubst %.txt,%.5,$(MAN1_TXT))
 DOC_MAN7=$(patsubst %.txt,%.7,$(MAN7_TXT))
 
 prefix?=$(HOME)
 bindir?=$(prefix)/bin
 mandir?=$(prefix)/man
 man1dir=$(mandir)/man1
+man5dir=$(mandir)/man5
 man7dir=$(mandir)/man7
 # DESTDIR=
 
@@ -53,15 +56,19 @@ all: html man
 
 html: $(DOC_HTML)
 
-$(DOC_HTML) $(DOC_MAN1) $(DOC_MAN7): asciidoc.conf
+$(DOC_HTML) $(DOC_MAN1) $(DOC_MAN5) $(DOC_MAN7): asciidoc.conf
 
-man: man1 man7
+man: man1 man5 man7
 man1: $(DOC_MAN1)
+man5: $(DOC_MAN5)
 man7: $(DOC_MAN7)
 
 install: man
-       $(INSTALL) -d -m755 $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir)
+       $(INSTALL) -d -m755 $(DESTDIR)$(man1dir)
+       $(INSTALL) -d -m755 $(DESTDIR)$(man5dir)
+       $(INSTALL) -d -m755 $(DESTDIR)$(man7dir)
        $(INSTALL) -m644 $(DOC_MAN1) $(DESTDIR)$(man1dir)
+       $(INSTALL) -m644 $(DOC_MAN5) $(DESTDIR)$(man5dir)
        $(INSTALL) -m644 $(DOC_MAN7) $(DESTDIR)$(man7dir)
 
 
@@ -99,7 +106,7 @@ cmd-list.made: cmd-list.perl $(MAN1_TXT)
 git.7 git.html: git.txt core-intro.txt
 
 clean:
-       rm -f *.xml *.xml+ *.html *.html+ *.1 *.7 howto-index.txt howto/*.html doc.dep
+       rm -f *.xml *.xml+ *.html *.html+ *.1 *.5 *.7 howto-index.txt howto/*.html doc.dep
        rm -f $(cmds_txt) *.made
 
 %.html : %.txt
@@ -109,7 +116,7 @@ clean:
                sed -e 's/@@GIT_VERSION@@/$(GIT_VERSION)/g' >$@+
        mv $@+ $@
 
-%.1 %.7 : %.xml
+%.1 %.5 %.7 : %.xml
        xmlto -m callouts.xsl man $<
 
 %.xml : %.txt
index 0381590..443802a 100755 (executable)
@@ -84,6 +84,7 @@ git-bundle                              mainporcelain
 git-cat-file                            plumbinginterrogators
 git-checkout-index                      plumbingmanipulators
 git-checkout                            mainporcelain
+git-check-attr                          purehelpers
 git-check-ref-format                    purehelpers
 git-cherry                              ancillaryinterrogators
 git-cherry-pick                         mainporcelain
index 2c0a666..b13ff3a 100644 (file)
@@ -525,6 +525,19 @@ merge.verbosity::
        conflicts, 2 outputs conflicts and file changes.  Level 5 and
        above outputs debugging information.  The default is level 2.
 
+merge.<driver>.name::
+       Defines a human readable name for a custom low-level
+       merge driver.  See gitlink:gitattributes[5] for details.
+
+merge.<driver>.driver::
+       Defines the command that implements a custom low-level
+       merge driver.  See gitlink:gitattributes[5] for details.
+
+merge.<driver>.recursive::
+       Names a low-level merge driver to be used when
+       performing an internal merge between common ancestors.
+       See gitlink:gitattributes[5] for details.
+
 pack.window::
        The size of the window used by gitlink:git-pack-objects[1] when no
        window size is given on the command line. Defaults to 10.
diff --git a/Documentation/git-check-attr.txt b/Documentation/git-check-attr.txt
new file mode 100644 (file)
index 0000000..ceb5195
--- /dev/null
@@ -0,0 +1,37 @@
+git-check-attr(1)
+=================
+
+NAME
+----
+git-check-attr - Display gitattributes information.
+
+
+SYNOPSIS
+--------
+'git-check-attr' attr... [--] pathname...
+
+DESCRIPTION
+-----------
+For every pathname, this command will list if each attr is 'unspecified',
+'set', or 'unset' as a gitattribute on that pathname.
+
+OPTIONS
+-------
+\--::
+       Interpret all preceding arguments as attributes, and all following
+       arguments as path names. If not supplied, only the first argument will
+       be treated as an attribute.
+
+
+Author
+------
+Written by Junio C Hamano <junkio@cox.net>
+
+Documentation
+--------------
+Documentation by James Bowes.
+
+GIT
+---
+Part of the gitlink:git[7] suite
+
diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt
new file mode 100644 (file)
index 0000000..ece58ab
--- /dev/null
@@ -0,0 +1,285 @@
+gitattributes(5)
+================
+
+NAME
+----
+gitattributes - defining attributes per path
+
+SYNOPSIS
+--------
+.gitattributes
+
+
+DESCRIPTION
+-----------
+
+A `gitattributes` file is a simple text file that gives
+`attributes` to pathnames.
+
+Each line in `gitattributes` file is of form:
+
+       glob    attr1 attr2 ...
+
+That is, a glob pattern followed by an attributes list,
+separated by whitespaces.  When the glob pattern matches the
+path in question, the attributes listed on the line are given to
+the path.
+
+Each attribute can be in one of these states for a given path:
+
+Set::
+
+       The path has the attribute with special value "true";
+       this is specified by listing only the name of the
+       attribute in the attribute list.
+
+Unset::
+
+       The path has the attribute with special value "false";
+       this is specified by listing the name of the attribute
+       prefixed with a dash `-` in the attribute list.
+
+Set to a value::
+
+       The path has the attribute with specified string value;
+       this is specified by listing the name of the attribute
+       followed by an equal sign `=` and its value in the
+       attribute list.
+
+Unspecified::
+
+       No glob pattern matches the path, and nothing says if
+       the path has or does not have the attribute.
+
+When more than one glob pattern matches the path, a later line
+overrides an earlier line.
+
+When deciding what attributes are assigned to a path, git
+consults `$GIT_DIR/info/attributes` file (which has the highest
+precedence), `.gitattributes` file in the same directory as the
+path in question, and its parent directories (the further the
+directory that contains `.gitattributes` is from the path in
+question, the lower its precedence).
+
+Sometimes you would need to override an setting of an attribute
+for a path to `unspecified` state.  This can be done by listing
+the name of the attribute prefixed with an exclamation point `!`.
+
+
+EFFECTS
+-------
+
+Certain operations by git can be influenced by assigning
+particular attributes to a path.  Currently, three operations
+are attributes-aware.
+
+Checking-out and checking-in
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The attribute `crlf` affects how the contents stored in the
+repository are copied to the working tree files when commands
+such as `git checkout` and `git merge` run.  It also affects how
+git stores the contents you prepare in the working tree in the
+repository upon `git add` and `git commit`.
+
+Set::
+
+       Setting the `crlf` attribute on a path is meant to mark
+       the path as a "text" file.  'core.autocrlf' conversion
+       takes place without guessing the content type by
+       inspection.
+
+Unset::
+
+       Unsetting the `crlf` attribute on a path is meant to
+       mark the path as a "binary" file.  The path never goes
+       through line endings conversion upon checkin/checkout.
+
+Unspecified::
+
+       Unspecified `crlf` attribute tells git to apply the
+       `core.autocrlf` conversion when the file content looks
+       like text.
+
+Set to string value "input"::
+
+       This is similar to setting the attribute to `true`, but
+       also forces git to act as if `core.autocrlf` is set to
+       `input` for the path.
+
+Any other value set to `crlf` attribute is ignored and git acts
+as if the attribute is left unspecified.
+
+
+The `core.autocrlf` conversion
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If the configuration variable `core.autocrlf` is false, no
+conversion is done.
+
+When `core.autocrlf` is true, it means that the platform wants
+CRLF line endings for files in the working tree, and you want to
+convert them back to the normal LF line endings when checking
+in to the repository.
+
+When `core.autocrlf` is set to "input", line endings are
+converted to LF upon checkin, but there is no conversion done
+upon checkout.
+
+
+Generating diff text
+~~~~~~~~~~~~~~~~~~~~
+
+The attribute `diff` affects if `git diff` generates textual
+patch for the path or just says `Binary files differ`.
+
+Set::
+
+       A path to which the `diff` attribute is set is treated
+       as text, even when they contain byte values that
+       normally never appear in text files, such as NUL.
+
+Unset::
+
+       A path to which the `diff` attribute is unset will
+       generate `Binary files differ`.
+
+Unspecified::
+
+       A path to which the `diff` attribute is unspecified
+       first gets its contents inspected, and if it looks like
+       text, it is treated as text.  Otherwise it would
+       generate `Binary files differ`.
+
+Any other value set to `diff` attribute is ignored and git acts
+as if the attribute is left unspecified.
+
+
+Performing a three-way merge
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The attribute `merge` affects how three versions of a file is
+merged when a file-level merge is necessary during `git merge`,
+and other programs such as `git revert` and `git cherry-pick`.
+
+Set::
+
+       Built-in 3-way merge driver is used to merge the
+       contents in a way similar to `merge` command of `RCS`
+       suite.  This is suitable for ordinary text files.
+
+Unset::
+
+       Take the version from the current branch as the
+       tentative merge result, and declare that the merge has
+       conflicts.  This is suitable for binary files that does
+       not have a well-defined merge semantics.
+
+Unspecified::
+
+       By default, this uses the same built-in 3-way merge
+       driver as is the case the `merge` attribute is set.
+       However, `merge.default` configuration variable can name
+       different merge driver to be used for paths to which the
+       `merge` attribute is unspecified.
+
+Any other string value::
+
+       3-way merge is performed using the specified custom
+       merge driver.  The built-in 3-way merge driver can be
+       explicitly specified by asking for "text" driver; the
+       built-in "take the current branch" driver can be
+       requested by "binary".
+
+
+Defining a custom merge driver
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The definition of a merge driver is done in `gitconfig` not
+`gitattributes` file, so strictly speaking this manual page is a
+wrong place to talk about it.  However...
+
+To define a custom merge driver `filfre`, add a section to your
+`$GIT_DIR/config` file (or `$HOME/.gitconfig` file) like this:
+
+----------------------------------------------------------------
+[merge "filfre"]
+       name = feel-free merge driver
+       driver = filfre %O %A %B
+       recursive = binary
+----------------------------------------------------------------
+
+The `merge.*.name` variable gives the driver a human-readable
+name.
+
+The `merge.*.driver` variable's value is used to construct a
+command to run to merge ancestor's version (`%O`), current
+version (`%A`) and the other branches' version (`%B`).  These
+three tokens are replaced with the names of temporary files that
+hold the contents of these versions when the command line is
+built.
+
+The merge driver is expected to leave the result of the merge in
+the file named with `%A` by overwriting it, and exit with zero
+status if it managed to merge them cleanly, or non-zero if there
+were conflicts.
+
+The `merge.*.recursive` variable specifies what other merge
+driver to use when the merge driver is called for an internal
+merge between common ancestors, when there are more than one.
+When left unspecified, the driver itself is used for both
+internal merge and the final merge.
+
+
+EXAMPLE
+-------
+
+If you have these three `gitattributes` file:
+
+----------------------------------------------------------------
+(in $GIT_DIR/info/attributes)
+
+a*     foo !bar -baz
+
+(in .gitattributes)
+abc    foo bar baz
+
+(in t/.gitattributes)
+ab*    merge=filfre
+abc    -foo -bar
+*.c    frotz
+----------------------------------------------------------------
+
+the attributes given to path `t/abc` are computed as follows:
+
+1. By examining `t/.gitattributes` (which is in the same
+   diretory as the path in question), git finds that the first
+   line matches.  `merge` attribute is set.  It also finds that
+   the second line matches, and attributes `foo` and `bar`
+   are unset.
+
+2. Then it examines `.gitattributes` (which is in the parent
+   directory), and finds that the first line matches, but
+   `t/.gitattributes` file already decided how `merge`, `foo`
+   and `bar` attributes should be given to this path, so it
+   leaves `foo` and `bar` unset.  Attribute `baz` is set.
+
+3. Finally it examines `$GIT_DIR/info/gitattributes`.  This file
+   is used to override the in-tree settings.  The first line is
+   a match, and `foo` is set, `bar` is reverted to unspecified
+   state, and `baz` is unset.
+
+As the result, the attributes assignement to `t/abc` becomes:
+
+----------------------------------------------------------------
+foo    set to true
+bar    unspecified
+baz    set to false
+merge  set to string value "filfre"
+frotz  unspecified
+----------------------------------------------------------------
+
+
+GIT
+---
+Part of the gitlink:git[7] suite
index 173c8b6..c9c2a5f 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -283,7 +283,7 @@ LIB_H = \
        diff.h object.h pack.h pkt-line.h quote.h refs.h list-objects.h sideband.h \
        run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h \
        tree-walk.h log-tree.h dir.h path-list.h unpack-trees.h builtin.h \
-       utf8.h reflog-walk.h patch-ids.h decorate.h
+       utf8.h reflog-walk.h patch-ids.h attr.h decorate.h
 
 DIFF_OBJS = \
        diff.o diff-lib.o diffcore-break.o diffcore-order.o \
@@ -305,7 +305,7 @@ LIB_OBJS = \
        write_or_die.o trace.o list-objects.o grep.o match-trees.o \
        alloc.o merge-file.o path-list.o help.o unpack-trees.o $(DIFF_OBJS) \
        color.o wt-status.o archive-zip.o archive-tar.o shallow.o utf8.o \
-       convert.o decorate.o
+       convert.o attr.o decorate.o
 
 BUILTIN_OBJS = \
        builtin-add.o \
@@ -316,6 +316,7 @@ BUILTIN_OBJS = \
        builtin-branch.o \
        builtin-bundle.o \
        builtin-cat-file.o \
+       builtin-check-attr.o \
        builtin-checkout-index.o \
        builtin-check-ref-format.o \
        builtin-commit-tree.o \
@@ -1032,9 +1033,10 @@ dist-doc:
        gzip -n -9 -f $(htmldocs).tar
        :
        rm -fr .doc-tmp-dir
-       mkdir .doc-tmp-dir .doc-tmp-dir/man1 .doc-tmp-dir/man7
+       mkdir -p .doc-tmp-dir/man1 .doc-tmp-dir/man5 .doc-tmp-dir/man7
        $(MAKE) -C Documentation DESTDIR=./ \
                man1dir=../.doc-tmp-dir/man1 \
+               man5dir=../.doc-tmp-dir/man5 \
                man7dir=../.doc-tmp-dir/man7 \
                install
        cd .doc-tmp-dir && $(TAR) cf ../$(manpages).tar .
diff --git a/attr.c b/attr.c
new file mode 100644 (file)
index 0000000..285e689
--- /dev/null
+++ b/attr.c
@@ -0,0 +1,563 @@
+#include "cache.h"
+#include "attr.h"
+
+const char git_attr__true[] = "(builtin)true";
+const char git_attr__false[] = "\0(builtin)false";
+static const char git_attr__unknown[] = "(builtin)unknown";
+#define ATTR__TRUE git_attr__true
+#define ATTR__FALSE git_attr__false
+#define ATTR__UNSET NULL
+#define ATTR__UNKNOWN git_attr__unknown
+
+/*
+ * The basic design decision here is that we are not going to have
+ * insanely large number of attributes.
+ *
+ * This is a randomly chosen prime.
+ */
+#define HASHSIZE 257
+
+#ifndef DEBUG_ATTR
+#define DEBUG_ATTR 0
+#endif
+
+struct git_attr {
+       struct git_attr *next;
+       unsigned h;
+       int attr_nr;
+       char name[FLEX_ARRAY];
+};
+static int attr_nr;
+
+static struct git_attr_check *check_all_attr;
+static struct git_attr *(git_attr_hash[HASHSIZE]);
+
+static unsigned hash_name(const char *name, int namelen)
+{
+       unsigned val = 0;
+       unsigned char c;
+
+       while (namelen--) {
+               c = *name++;
+               val = ((val << 7) | (val >> 22)) ^ c;
+       }
+       return val;
+}
+
+static int invalid_attr_name(const char *name, int namelen)
+{
+       /*
+        * Attribute name cannot begin with '-' and from
+        * [-A-Za-z0-9_.].  We'd specifically exclude '=' for now,
+        * as we might later want to allow non-binary value for
+        * attributes, e.g. "*.svg      merge=special-merge-program-for-svg"
+        */
+       if (*name == '-')
+               return -1;
+       while (namelen--) {
+               char ch = *name++;
+               if (! (ch == '-' || ch == '.' || ch == '_' ||
+                      ('0' <= ch && ch <= '9') ||
+                      ('a' <= ch && ch <= 'z') ||
+                      ('A' <= ch && ch <= 'Z')) )
+                       return -1;
+       }
+       return 0;
+}
+
+struct git_attr *git_attr(const char *name, int len)
+{
+       unsigned hval = hash_name(name, len);
+       unsigned pos = hval % HASHSIZE;
+       struct git_attr *a;
+
+       for (a = git_attr_hash[pos]; a; a = a->next) {
+               if (a->h == hval &&
+                   !memcmp(a->name, name, len) && !a->name[len])
+                       return a;
+       }
+
+       if (invalid_attr_name(name, len))
+               return NULL;
+
+       a = xmalloc(sizeof(*a) + len + 1);
+       memcpy(a->name, name, len);
+       a->name[len] = 0;
+       a->h = hval;
+       a->next = git_attr_hash[pos];
+       a->attr_nr = attr_nr++;
+       git_attr_hash[pos] = a;
+
+       check_all_attr = xrealloc(check_all_attr,
+                                 sizeof(*check_all_attr) * attr_nr);
+       check_all_attr[a->attr_nr].attr = a;
+       check_all_attr[a->attr_nr].value = ATTR__UNKNOWN;
+       return a;
+}
+
+/*
+ * .gitattributes file is one line per record, each of which is
+ *
+ * (1) glob pattern.
+ * (2) whitespace
+ * (3) whitespace separated list of attribute names, each of which
+ *     could be prefixed with '-' to mean "set to false", '!' to mean
+ *     "unset".
+ */
+
+/* What does a matched pattern decide? */
+struct attr_state {
+       struct git_attr *attr;
+       const char *setto;
+};
+
+struct match_attr {
+       union {
+               char *pattern;
+               struct git_attr *attr;
+       } u;
+       char is_macro;
+       unsigned num_attr;
+       struct attr_state state[FLEX_ARRAY];
+};
+
+static const char blank[] = " \t\r\n";
+
+static const char *parse_attr(const char *src, int lineno, const char *cp,
+                             int *num_attr, struct match_attr *res)
+{
+       const char *ep, *equals;
+       int len;
+
+       ep = cp + strcspn(cp, blank);
+       equals = strchr(cp, '=');
+       if (equals && ep < equals)
+               equals = NULL;
+       if (equals)
+               len = equals - cp;
+       else
+               len = ep - cp;
+       if (!res) {
+               if (*cp == '-' || *cp == '!') {
+                       cp++;
+                       len--;
+               }
+               if (invalid_attr_name(cp, len)) {
+                       fprintf(stderr,
+                               "%.*s is not a valid attribute name: %s:%d\n",
+                               len, cp, src, lineno);
+                       return NULL;
+               }
+       } else {
+               struct attr_state *e;
+
+               e = &(res->state[*num_attr]);
+               if (*cp == '-' || *cp == '!') {
+                       e->setto = (*cp == '-') ? ATTR__FALSE : ATTR__UNSET;
+                       cp++;
+                       len--;
+               }
+               else if (!equals)
+                       e->setto = ATTR__TRUE;
+               else {
+                       char *value;
+                       int vallen = ep - equals;
+                       value = xmalloc(vallen);
+                       memcpy(value, equals+1, vallen-1);
+                       value[vallen-1] = 0;
+                       e->setto = value;
+               }
+               e->attr = git_attr(cp, len);
+       }
+       (*num_attr)++;
+       return ep + strspn(ep, blank);
+}
+
+static struct match_attr *parse_attr_line(const char *line, const char *src,
+                                         int lineno, int macro_ok)
+{
+       int namelen;
+       int num_attr;
+       const char *cp, *name;
+       struct match_attr *res = NULL;
+       int pass;
+       int is_macro;
+
+       cp = line + strspn(line, blank);
+       if (!*cp || *cp == '#')
+               return NULL;
+       name = cp;
+       namelen = strcspn(name, blank);
+       if (strlen(ATTRIBUTE_MACRO_PREFIX) < namelen &&
+           !prefixcmp(name, ATTRIBUTE_MACRO_PREFIX)) {
+               if (!macro_ok) {
+                       fprintf(stderr, "%s not allowed: %s:%d\n",
+                               name, src, lineno);
+                       return NULL;
+               }
+               is_macro = 1;
+               name += strlen(ATTRIBUTE_MACRO_PREFIX);
+               name += strspn(name, blank);
+               namelen = strcspn(name, blank);
+               if (invalid_attr_name(name, namelen)) {
+                       fprintf(stderr,
+                               "%.*s is not a valid attribute name: %s:%d\n",
+                               namelen, name, src, lineno);
+                       return NULL;
+               }
+       }
+       else
+               is_macro = 0;
+
+       for (pass = 0; pass < 2; pass++) {
+               /* pass 0 counts and allocates, pass 1 fills */
+               num_attr = 0;
+               cp = name + namelen;
+               cp = cp + strspn(cp, blank);
+               while (*cp)
+                       cp = parse_attr(src, lineno, cp, &num_attr, res);
+               if (pass)
+                       break;
+               res = xcalloc(1,
+                             sizeof(*res) +
+                             sizeof(struct attr_state) * num_attr +
+                             (is_macro ? 0 : namelen + 1));
+               if (is_macro)
+                       res->u.attr = git_attr(name, namelen);
+               else {
+                       res->u.pattern = (char*)&(res->state[num_attr]);
+                       memcpy(res->u.pattern, name, namelen);
+                       res->u.pattern[namelen] = 0;
+               }
+               res->is_macro = is_macro;
+               res->num_attr = num_attr;
+       }
+       return res;
+}
+
+/*
+ * Like info/exclude and .gitignore, the attribute information can
+ * come from many places.
+ *
+ * (1) .gitattribute file of the same directory;
+ * (2) .gitattribute file of the parent directory if (1) does not have
+ *      any match; this goes recursively upwards, just like .gitignore.
+ * (3) $GIT_DIR/info/attributes, which overrides both of the above.
+ *
+ * In the same file, later entries override the earlier match, so in the
+ * global list, we would have entries from info/attributes the earliest
+ * (reading the file from top to bottom), .gitattribute of the root
+ * directory (again, reading the file from top to bottom) down to the
+ * current directory, and then scan the list backwards to find the first match.
+ * This is exactly the same as what excluded() does in dir.c to deal with
+ * .gitignore
+ */
+
+static struct attr_stack {
+       struct attr_stack *prev;
+       char *origin;
+       unsigned num_matches;
+       struct match_attr **attrs;
+} *attr_stack;
+
+static void free_attr_elem(struct attr_stack *e)
+{
+       int i;
+       free(e->origin);
+       for (i = 0; i < e->num_matches; i++) {
+               struct match_attr *a = e->attrs[i];
+               int j;
+               for (j = 0; j < a->num_attr; j++) {
+                       const char *setto = a->state[j].setto;
+                       if (setto == ATTR__TRUE ||
+                           setto == ATTR__FALSE ||
+                           setto == ATTR__UNSET ||
+                           setto == ATTR__UNKNOWN)
+                               ;
+                       else
+                               free((char*) setto);
+               }
+               free(a);
+       }
+       free(e);
+}
+
+static const char *builtin_attr[] = {
+       "[attr]binary -diff -crlf",
+       NULL,
+};
+
+static struct attr_stack *read_attr_from_array(const char **list)
+{
+       struct attr_stack *res;
+       const char *line;
+       int lineno = 0;
+
+       res = xcalloc(1, sizeof(*res));
+       while ((line = *(list++)) != NULL) {
+               struct match_attr *a;
+
+               a = parse_attr_line(line, "[builtin]", ++lineno, 1);
+               if (!a)
+                       continue;
+               res->attrs = xrealloc(res->attrs, res->num_matches + 1);
+               res->attrs[res->num_matches++] = a;
+       }
+       return res;
+}
+
+static struct attr_stack *read_attr_from_file(const char *path, int macro_ok)
+{
+       FILE *fp;
+       struct attr_stack *res;
+       char buf[2048];
+       int lineno = 0;
+
+       res = xcalloc(1, sizeof(*res));
+       fp = fopen(path, "r");
+       if (!fp)
+               return res;
+
+       while (fgets(buf, sizeof(buf), fp)) {
+               struct match_attr *a;
+
+               a = parse_attr_line(buf, path, ++lineno, macro_ok);
+               if (!a)
+                       continue;
+               res->attrs = xrealloc(res->attrs, res->num_matches + 1);
+               res->attrs[res->num_matches++] = a;
+       }
+       fclose(fp);
+       return res;
+}
+
+#if DEBUG_ATTR
+static void debug_info(const char *what, struct attr_stack *elem)
+{
+       fprintf(stderr, "%s: %s\n", what, elem->origin ? elem->origin : "()");
+}
+static void debug_set(const char *what, const char *match, struct git_attr *attr, void *v)
+{
+       const char *value = v;
+
+       if (ATTR_TRUE(value))
+               value = "set";
+       else if (ATTR_FALSE(value))
+               value = "unset";
+       else if (ATTR_UNSET(value))
+               value = "unspecified";
+
+       fprintf(stderr, "%s: %s => %s (%s)\n",
+               what, attr->name, (char *) value, match);
+}
+#define debug_push(a) debug_info("push", (a))
+#define debug_pop(a) debug_info("pop", (a))
+#else
+#define debug_push(a) do { ; } while (0)
+#define debug_pop(a) do { ; } while (0)
+#define debug_set(a,b,c,d) do { ; } while (0)
+#endif
+
+static void bootstrap_attr_stack(void)
+{
+       if (!attr_stack) {
+               struct attr_stack *elem;
+
+               elem = read_attr_from_array(builtin_attr);
+               elem->origin = NULL;
+               elem->prev = attr_stack;
+               attr_stack = elem;
+
+               elem = read_attr_from_file(GITATTRIBUTES_FILE, 1);
+               elem->origin = strdup("");
+               elem->prev = attr_stack;
+               attr_stack = elem;
+               debug_push(elem);
+
+               elem = read_attr_from_file(git_path(INFOATTRIBUTES_FILE), 1);
+               elem->origin = NULL;
+               elem->prev = attr_stack;
+               attr_stack = elem;
+       }
+}
+
+static void prepare_attr_stack(const char *path, int dirlen)
+{
+       struct attr_stack *elem, *info;
+       int len;
+       char pathbuf[PATH_MAX];
+
+       /*
+        * At the bottom of the attribute stack is the built-in
+        * set of attribute definitions.  Then, contents from
+        * .gitattribute files from directories closer to the
+        * root to the ones in deeper directories are pushed
+        * to the stack.  Finally, at the very top of the stack
+        * we always keep the contents of $GIT_DIR/info/attributes.
+        *
+        * When checking, we use entries from near the top of the
+        * stack, preferring $GIT_DIR/info/attributes, then
+        * .gitattributes in deeper directories to shallower ones,
+        * and finally use the built-in set as the default.
+        */
+       if (!attr_stack)
+               bootstrap_attr_stack();
+
+       /*
+        * Pop the "info" one that is always at the top of the stack.
+        */
+       info = attr_stack;
+       attr_stack = info->prev;
+
+       /*
+        * Pop the ones from directories that are not the prefix of
+        * the path we are checking.
+        */
+       while (attr_stack && attr_stack->origin) {
+               int namelen = strlen(attr_stack->origin);
+
+               elem = attr_stack;
+               if (namelen <= dirlen &&
+                   !strncmp(elem->origin, path, namelen))
+                       break;
+
+               debug_pop(elem);
+               attr_stack = elem->prev;
+               free_attr_elem(elem);
+       }
+
+       /*
+        * Read from parent directories and push them down
+        */
+       while (1) {
+               char *cp;
+
+               len = strlen(attr_stack->origin);
+               if (dirlen <= len)
+                       break;
+               memcpy(pathbuf, path, dirlen);
+               memcpy(pathbuf + dirlen, "/", 2);
+               cp = strchr(pathbuf + len + 1, '/');
+               strcpy(cp + 1, GITATTRIBUTES_FILE);
+               elem = read_attr_from_file(pathbuf, 0);
+               *cp = '\0';
+               elem->origin = strdup(pathbuf);
+               elem->prev = attr_stack;
+               attr_stack = elem;
+               debug_push(elem);
+       }
+
+       /*
+        * Finally push the "info" one at the top of the stack.
+        */
+       info->prev = attr_stack;
+       attr_stack = info;
+}
+
+static int path_matches(const char *pathname, int pathlen,
+                       const char *pattern,
+                       const char *base, int baselen)
+{
+       if (!strchr(pattern, '/')) {
+               /* match basename */
+               const char *basename = strrchr(pathname, '/');
+               basename = basename ? basename + 1 : pathname;
+               return (fnmatch(pattern, basename, 0) == 0);
+       }
+       /*
+        * match with FNM_PATHNAME; the pattern has base implicitly
+        * in front of it.
+        */
+       if (*pattern == '/')
+               pattern++;
+       if (pathlen < baselen ||
+           (baselen && pathname[baselen - 1] != '/') ||
+           strncmp(pathname, base, baselen))
+               return 0;
+       return fnmatch(pattern, pathname + baselen, FNM_PATHNAME) == 0;
+}
+
+static int fill_one(const char *what, struct match_attr *a, int rem)
+{
+       struct git_attr_check *check = check_all_attr;
+       int i;
+
+       for (i = 0; 0 < rem && i < a->num_attr; i++) {
+               struct git_attr *attr = a->state[i].attr;
+               const char **n = &(check[attr->attr_nr].value);
+               const char *v = a->state[i].setto;
+
+               if (*n == ATTR__UNKNOWN) {
+                       debug_set(what, a->u.pattern, attr, v);
+                       *n = v;
+                       rem--;
+               }
+       }
+       return rem;
+}
+
+static int fill(const char *path, int pathlen, struct attr_stack *stk, int rem)
+{
+       int i;
+       const char *base = stk->origin ? stk->origin : "";
+
+       for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) {
+               struct match_attr *a = stk->attrs[i];
+               if (a->is_macro)
+                       continue;
+               if (path_matches(path, pathlen,
+                                a->u.pattern, base, strlen(base)))
+                       rem = fill_one("fill", a, rem);
+       }
+       return rem;
+}
+
+static int macroexpand(struct attr_stack *stk, int rem)
+{
+       int i;
+       struct git_attr_check *check = check_all_attr;
+
+       for (i = stk->num_matches - 1; 0 < rem && 0 <= i; i--) {
+               struct match_attr *a = stk->attrs[i];
+               if (!a->is_macro)
+                       continue;
+               if (check[a->u.attr->attr_nr].value != ATTR__TRUE)
+                       continue;
+               rem = fill_one("expand", a, rem);
+       }
+       return rem;
+}
+
+int git_checkattr(const char *path, int num, struct git_attr_check *check)
+{
+       struct attr_stack *stk;
+       const char *cp;
+       int dirlen, pathlen, i, rem;
+
+       bootstrap_attr_stack();
+       for (i = 0; i < attr_nr; i++)
+               check_all_attr[i].value = ATTR__UNKNOWN;
+
+       pathlen = strlen(path);
+       cp = strrchr(path, '/');
+       if (!cp)
+               dirlen = 0;
+       else
+               dirlen = cp - path;
+       prepare_attr_stack(path, dirlen);
+       rem = attr_nr;
+       for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
+               rem = fill(path, pathlen, stk, rem);
+
+       for (stk = attr_stack; 0 < rem && stk; stk = stk->prev)
+               rem = macroexpand(stk, rem);
+
+       for (i = 0; i < num; i++) {
+               const char *value = check_all_attr[check[i].attr->attr_nr].value;
+               if (value == ATTR__UNKNOWN)
+                       value = ATTR__UNSET;
+               check[i].value = value;
+       }
+
+       return 0;
+}
diff --git a/attr.h b/attr.h
new file mode 100644 (file)
index 0000000..f1c2038
--- /dev/null
+++ b/attr.h
@@ -0,0 +1,34 @@
+#ifndef ATTR_H
+#define ATTR_H
+
+/* An attribute is a pointer to this opaque structure */
+struct git_attr;
+
+/*
+ * Given a string, return the gitattribute object that
+ * corresponds to it.
+ */
+struct git_attr *git_attr(const char *, int);
+
+/* Internal use */
+extern const char git_attr__true[];
+extern const char git_attr__false[];
+
+/* For public to check git_attr_check results */
+#define ATTR_TRUE(v) ((v) == git_attr__true)
+#define ATTR_FALSE(v) ((v) == git_attr__false)
+#define ATTR_UNSET(v) ((v) == NULL)
+
+/*
+ * Send one or more git_attr_check to git_checkattr(), and
+ * each 'value' member tells what its value is.
+ * Unset one is returned as NULL.
+ */
+struct git_attr_check {
+       struct git_attr *attr;
+       const char *value;
+};
+
+int git_checkattr(const char *path, int, struct git_attr_check *);
+
+#endif /* ATTR_H */
index 94311e7..f94d0db 100644 (file)
@@ -1475,8 +1475,8 @@ static int read_old_data(struct stat *st, const char *path, char **buf_p, unsign
                }
                close(fd);
                nsize = got;
-               nbuf = buf;
-               if (convert_to_git(path, &nbuf, &nsize)) {
+               nbuf = convert_to_git(path, buf, &nsize);
+               if (nbuf) {
                        free(buf);
                        *buf_p = nbuf;
                        *alloc_p = nsize;
@@ -2355,9 +2355,8 @@ static void add_index_file(const char *path, unsigned mode, void *buf, unsigned
 
 static int try_create_file(const char *path, unsigned int mode, const char *buf, unsigned long size)
 {
-       int fd, converted;
+       int fd;
        char *nbuf;
-       unsigned long nsize;
 
        if (has_symlinks && S_ISLNK(mode))
                /* Although buf:size is counted string, it also is NUL
@@ -2369,13 +2368,10 @@ static int try_create_file(const char *path, unsigned int mode, const char *buf,
        if (fd < 0)
                return -1;
 
-       nsize = size;
-       nbuf = (char *) buf;
-       converted = convert_to_working_tree(path, &nbuf, &nsize);
-       if (converted) {
+       nbuf = convert_to_working_tree(path, buf, &size);
+       if (nbuf)
                buf = nbuf;
-               size = nsize;
-       }
+
        while (size) {
                int written = xwrite(fd, buf, size);
                if (written < 0)
@@ -2387,7 +2383,7 @@ static int try_create_file(const char *path, unsigned int mode, const char *buf,
        }
        if (close(fd) < 0)
                die("closing file %s: %s", path, strerror(errno));
-       if (converted)
+       if (nbuf)
                free(nbuf);
        return 0;
 }
diff --git a/builtin-check-attr.c b/builtin-check-attr.c
new file mode 100644 (file)
index 0000000..9d77f76
--- /dev/null
@@ -0,0 +1,59 @@
+#include "builtin.h"
+#include "attr.h"
+#include "quote.h"
+
+static const char check_attr_usage[] =
+"git-check-attr attr... [--] pathname...";
+
+int cmd_check_attr(int argc, const char **argv, const char *prefix)
+{
+       struct git_attr_check *check;
+       int cnt, i, doubledash;
+
+       doubledash = -1;
+       for (i = 1; doubledash < 0 && i < argc; i++) {
+               if (!strcmp(argv[i], "--"))
+                       doubledash = i;
+       }
+
+       /* If there is no double dash, we handle only one attribute */
+       if (doubledash < 0) {
+               cnt = 1;
+               doubledash = 1;
+       } else
+               cnt = doubledash - 1;
+       doubledash++;
+
+       if (cnt <= 0 || argc < doubledash)
+               usage(check_attr_usage);
+       check = xcalloc(cnt, sizeof(*check));
+       for (i = 0; i < cnt; i++) {
+               const char *name;
+               struct git_attr *a;
+               name = argv[i + 1];
+               a = git_attr(name, strlen(name));
+               if (!a)
+                       return error("%s: not a valid attribute name", name);
+               check[i].attr = a;
+       }
+
+       for (i = doubledash; i < argc; i++) {
+               int j;
+               if (git_checkattr(argv[i], cnt, check))
+                       die("git_checkattr died");
+               for (j = 0; j < cnt; j++) {
+                       const char *value = check[j].value;
+
+                       if (ATTR_TRUE(value))
+                               value = "set";
+                       else if (ATTR_FALSE(value))
+                               value = "unset";
+                       else if (ATTR_UNSET(value))
+                               value = "unspecified";
+
+                       write_name_quoted("", 0, argv[i], 1, stdout);
+                       printf(": %s: %s\n", argv[j+1], value);
+               }
+       }
+       return 0;
+}
index af203e9..d3f3a74 100644 (file)
--- a/builtin.h
+++ b/builtin.h
@@ -22,6 +22,7 @@ extern int cmd_branch(int argc, const char **argv, const char *prefix);
 extern int cmd_bundle(int argc, const char **argv, const char *prefix);
 extern int cmd_cat_file(int argc, const char **argv, const char *prefix);
 extern int cmd_checkout_index(int argc, const char **argv, const char *prefix);
+extern int cmd_check_attr(int argc, const char **argv, const char *prefix);
 extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry(int argc, const char **argv, const char *prefix);
 extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix);
diff --git a/cache.h b/cache.h
index 8747d01..05f1885 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -169,6 +169,9 @@ enum object_type {
 #define CONFIG_ENVIRONMENT "GIT_CONFIG"
 #define CONFIG_LOCAL_ENVIRONMENT "GIT_CONFIG_LOCAL"
 #define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH"
+#define GITATTRIBUTES_FILE ".gitattributes"
+#define INFOATTRIBUTES_FILE "info/attributes"
+#define ATTRIBUTE_MACRO_PREFIX "[attr]"
 
 extern int is_bare_repository_cfg;
 extern int is_bare_repository(void);
@@ -224,6 +227,7 @@ extern int refresh_cache(unsigned int flags);
 
 struct lock_file {
        struct lock_file *next;
+       pid_t owner;
        char on_list;
        char filename[PATH_MAX];
 };
@@ -512,8 +516,8 @@ extern void trace_printf(const char *format, ...);
 extern void trace_argv_printf(const char **argv, int count, const char *format, ...);
 
 /* convert.c */
-extern int convert_to_git(const char *path, char **bufp, unsigned long *sizep);
-extern int convert_to_working_tree(const char *path, char **bufp, unsigned long *sizep);
+extern char *convert_to_git(const char *path, const char *src, unsigned long *sizep);
+extern char *convert_to_working_tree(const char *path, const char *src, unsigned long *sizep);
 
 /* match-trees.c */
 void shift_tree(const unsigned char *, const unsigned char *, unsigned char *, int);
index 898bfe3..37239ac 100644 (file)
--- a/convert.c
+++ b/convert.c
@@ -1,4 +1,6 @@
 #include "cache.h"
+#include "attr.h"
+
 /*
  * convert.c - convert a file when checking it out and checking it in.
  *
@@ -8,6 +10,11 @@
  * translation when the "auto_crlf" option is set.
  */
 
+#define CRLF_GUESS     (-1)
+#define CRLF_BINARY    0
+#define CRLF_TEXT      1
+#define CRLF_INPUT     2
+
 struct text_stat {
        /* CR, LF and CRLF counts */
        unsigned cr, lf, crlf;
@@ -72,115 +79,171 @@ static int is_binary(unsigned long size, struct text_stat *stats)
        return 0;
 }
 
-int convert_to_git(const char *path, char **bufp, unsigned long *sizep)
+static char *crlf_to_git(const char *path, const char *src, unsigned long *sizep, int action)
 {
-       char *buffer, *nbuf;
+       char *buffer, *dst;
        unsigned long size, nsize;
        struct text_stat stats;
 
-       /*
-        * FIXME! Other pluggable conversions should go here,
-        * based on filename patterns. Right now we just do the
-        * stupid auto-CRLF one.
-        */
-       if (!auto_crlf)
-               return 0;
+       if ((action == CRLF_BINARY) || (action == CRLF_GUESS && !auto_crlf))
+               return NULL;
 
        size = *sizep;
        if (!size)
-               return 0;
-       buffer = *bufp;
+               return NULL;
 
-       gather_stats(buffer, size, &stats);
+       gather_stats(src, size, &stats);
 
        /* No CR? Nothing to convert, regardless. */
        if (!stats.cr)
-               return 0;
-
-       /*
-        * We're currently not going to even try to convert stuff
-        * that has bare CR characters. Does anybody do that crazy
-        * stuff?
-        */
-       if (stats.cr != stats.crlf)
-               return 0;
-
-       /*
-        * And add some heuristics for binary vs text, of course...
-        */
-       if (is_binary(size, &stats))
-               return 0;
+               return NULL;
+
+       if (action == CRLF_GUESS) {
+               /*
+                * We're currently not going to even try to convert stuff
+                * that has bare CR characters. Does anybody do that crazy
+                * stuff?
+                */
+               if (stats.cr != stats.crlf)
+                       return NULL;
+
+               /*
+                * And add some heuristics for binary vs text, of course...
+                */
+               if (is_binary(size, &stats))
+                       return NULL;
+       }
 
        /*
         * Ok, allocate a new buffer, fill it in, and return true
         * to let the caller know that we switched buffers on it.
         */
        nsize = size - stats.crlf;
-       nbuf = xmalloc(nsize);
-       *bufp = nbuf;
+       buffer = xmalloc(nsize);
        *sizep = nsize;
-       do {
-               unsigned char c = *buffer++;
-               if (c != '\r')
-                       *nbuf++ = c;
-       } while (--size);
 
-       return 1;
+       dst = buffer;
+       if (action == CRLF_GUESS) {
+               /*
+                * If we guessed, we already know we rejected a file with
+                * lone CR, and we can strip a CR without looking at what
+                * follow it.
+                */
+               do {
+                       unsigned char c = *src++;
+                       if (c != '\r')
+                               *dst++ = c;
+               } while (--size);
+       } else {
+               do {
+                       unsigned char c = *src++;
+                       if (! (c == '\r' && (1 < size && *buffer == '\n')))
+                               *dst++ = c;
+               } while (--size);
+       }
+
+       return buffer;
 }
 
-int convert_to_working_tree(const char *path, char **bufp, unsigned long *sizep)
+static char *crlf_to_worktree(const char *path, const char *src, unsigned long *sizep, int action)
 {
-       char *buffer, *nbuf;
+       char *buffer, *dst;
        unsigned long size, nsize;
        struct text_stat stats;
        unsigned char last;
 
-       /*
-        * FIXME! Other pluggable conversions should go here,
-        * based on filename patterns. Right now we just do the
-        * stupid auto-CRLF one.
-        */
-       if (auto_crlf <= 0)
-               return 0;
+       if ((action == CRLF_BINARY) || (action == CRLF_INPUT) ||
+           (action == CRLF_GUESS && auto_crlf <= 0))
+               return NULL;
 
        size = *sizep;
        if (!size)
-               return 0;
-       buffer = *bufp;
+               return NULL;
 
-       gather_stats(buffer, size, &stats);
+       gather_stats(src, size, &stats);
 
        /* No LF? Nothing to convert, regardless. */
        if (!stats.lf)
-               return 0;
+               return NULL;
 
        /* Was it already in CRLF format? */
        if (stats.lf == stats.crlf)
-               return 0;
+               return NULL;
 
-       /* If we have any bare CR characters, we're not going to touch it */
-       if (stats.cr != stats.crlf)
-               return 0;
+       if (action == CRLF_GUESS) {
+               /* If we have any bare CR characters, we're not going to touch it */
+               if (stats.cr != stats.crlf)
+                       return NULL;
 
-       if (is_binary(size, &stats))
-               return 0;
+               if (is_binary(size, &stats))
+                       return NULL;
+       }
 
        /*
         * Ok, allocate a new buffer, fill it in, and return true
         * to let the caller know that we switched buffers on it.
         */
        nsize = size + stats.lf - stats.crlf;
-       nbuf = xmalloc(nsize);
-       *bufp = nbuf;
+       buffer = xmalloc(nsize);
        *sizep = nsize;
        last = 0;
+
+       dst = buffer;
        do {
-               unsigned char c = *buffer++;
+               unsigned char c = *src++;
                if (c == '\n' && last != '\r')
-                       *nbuf++ = '\r';
-               *nbuf++ = c;
+                       *dst++ = '\r';
+               *dst++ = c;
                last = c;
        } while (--size);
 
-       return 1;
+       return buffer;
+}
+
+static void setup_convert_check(struct git_attr_check *check)
+{
+       static struct git_attr *attr_crlf;
+
+       if (!attr_crlf)
+               attr_crlf = git_attr("crlf", 4);
+       check->attr = attr_crlf;
+}
+
+static int git_path_check_crlf(const char *path, struct git_attr_check *check)
+{
+       const char *value = check->value;
+
+       if (ATTR_TRUE(value))
+               return CRLF_TEXT;
+       else if (ATTR_FALSE(value))
+               return CRLF_BINARY;
+       else if (ATTR_UNSET(value))
+               ;
+       else if (!strcmp(value, "input"))
+               return CRLF_INPUT;
+       return CRLF_GUESS;
+}
+
+char *convert_to_git(const char *path, const char *src, unsigned long *sizep)
+{
+       struct git_attr_check check[1];
+       int crlf = CRLF_GUESS;
+
+       setup_convert_check(check);
+       if (!git_checkattr(path, 1, check)) {
+               crlf = git_path_check_crlf(path, check);
+       }
+       return crlf_to_git(path, src, sizep, crlf);
+}
+
+char *convert_to_working_tree(const char *path, const char *src, unsigned long *sizep)
+{
+       struct git_attr_check check[1];
+       int crlf = CRLF_GUESS;
+
+       setup_convert_check(check);
+       if (!git_checkattr(path, 1, check)) {
+               crlf = git_path_check_crlf(path, check);
+       }
+       return crlf_to_worktree(path, src, sizep, crlf);
 }
diff --git a/diff.c b/diff.c
index 1e8e689..f516664 100644 (file)
--- a/diff.c
+++ b/diff.c
@@ -8,6 +8,7 @@
 #include "delta.h"
 #include "xdiff-interface.h"
 #include "color.h"
+#include "attr.h"
 
 #ifdef NO_FAST_WORKING_DIRECTORY
 #define FAST_WORKING_DIRECTORY 0
@@ -1051,13 +1052,44 @@ static void emit_binary_diff(mmfile_t *one, mmfile_t *two)
        emit_binary_diff_body(two, one);
 }
 
+static void setup_diff_attr_check(struct git_attr_check *check)
+{
+       static struct git_attr *attr_diff;
+
+       if (!attr_diff)
+               attr_diff = git_attr("diff", 4);
+       check->attr = attr_diff;
+}
+
 #define FIRST_FEW_BYTES 8000
-static int mmfile_is_binary(mmfile_t *mf)
+static int file_is_binary(struct diff_filespec *one)
 {
-       long sz = mf->size;
+       unsigned long sz;
+       struct git_attr_check attr_diff_check;
+
+       setup_diff_attr_check(&attr_diff_check);
+       if (!git_checkattr(one->path, 1, &attr_diff_check)) {
+               const char *value = attr_diff_check.value;
+               if (ATTR_TRUE(value))
+                       return 0;
+               else if (ATTR_FALSE(value))
+                       return 1;
+               else if (ATTR_UNSET(value))
+                       ;
+               else
+                       die("unknown value %s given to 'diff' attribute",
+                           value);
+       }
+
+       if (!one->data) {
+               if (!DIFF_FILE_VALID(one))
+                       return 0;
+               diff_populate_filespec(one, 0);
+       }
+       sz = one->size;
        if (FIRST_FEW_BYTES < sz)
                sz = FIRST_FEW_BYTES;
-       return !!memchr(mf->ptr, 0, sz);
+       return !!memchr(one->data, 0, sz);
 }
 
 static void builtin_diff(const char *name_a,
@@ -1114,7 +1146,7 @@ static void builtin_diff(const char *name_a,
        if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
                die("unable to read files to diff");
 
-       if (!o->text && (mmfile_is_binary(&mf1) || mmfile_is_binary(&mf2))) {
+       if (!o->text && (file_is_binary(one) || file_is_binary(two))) {
                /* Quite common confusing case */
                if (mf1.size == mf2.size &&
                    !memcmp(mf1.ptr, mf2.ptr, mf1.size))
@@ -1190,7 +1222,7 @@ static void builtin_diffstat(const char *name_a, const char *name_b,
        if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
                die("unable to read files to diff");
 
-       if (mmfile_is_binary(&mf1) || mmfile_is_binary(&mf2)) {
+       if (file_is_binary(one) || file_is_binary(two)) {
                data->is_binary = 1;
                data->added = mf2.size;
                data->deleted = mf1.size;
@@ -1228,7 +1260,7 @@ static void builtin_checkdiff(const char *name_a, const char *name_b,
        if (fill_mmfile(&mf1, one) < 0 || fill_mmfile(&mf2, two) < 0)
                die("unable to read files to diff");
 
-       if (mmfile_is_binary(&mf2))
+       if (file_is_binary(two))
                return;
        else {
                /* Crazy xdl interfaces.. */
@@ -1481,9 +1513,9 @@ int diff_populate_filespec(struct diff_filespec *s, int size_only)
                /*
                 * Convert from working tree format to canonical git format
                 */
-               buf = s->data;
                size = s->size;
-               if (convert_to_git(s->path, &buf, &size)) {
+               buf = convert_to_git(s->path, s->data, &size);
+               if (buf) {
                        munmap(s->data, s->size);
                        s->should_munmap = 0;
                        s->data = buf;
@@ -1825,8 +1857,8 @@ static void run_diff(struct diff_filepair *p, struct diff_options *o)
 
                if (o->binary) {
                        mmfile_t mf;
-                       if ((!fill_mmfile(&mf, one) && mmfile_is_binary(&mf)) ||
-                           (!fill_mmfile(&mf, two) && mmfile_is_binary(&mf)))
+                       if ((!fill_mmfile(&mf, one) && file_is_binary(one)) ||
+                           (!fill_mmfile(&mf, two) && file_is_binary(two)))
                                abbrev = 40;
                }
                len += snprintf(msg + len, sizeof(msg) - len,
@@ -2721,7 +2753,7 @@ static int diff_get_patch_id(struct diff_options *options, unsigned char *sha1)
                        return error("unable to read files to diff");
 
                /* Maybe hash p->two? into the patch id? */
-               if (mmfile_is_binary(&mf2))
+               if (file_is_binary(p->two))
                        continue;
 
                len1 = remove_space(p->one->path, strlen(p->one->path));
diff --git a/entry.c b/entry.c
index 50ffae4..84f7802 100644 (file)
--- a/entry.c
+++ b/entry.c
@@ -82,7 +82,7 @@ static int write_entry(struct cache_entry *ce, char *path, struct checkout *stat
 
        switch (ntohl(ce->ce_mode) & S_IFMT) {
                char *buf, *new;
-               unsigned long size, nsize;
+               unsigned long size;
 
        case S_IFREG:
                new = read_blob_entry(ce, path, &size);
@@ -103,12 +103,10 @@ static int write_entry(struct cache_entry *ce, char *path, struct checkout *stat
                /*
                 * Convert from git internal format to working tree format
                 */
-               buf = new;
-               nsize = size;
-               if (convert_to_working_tree(ce->name, &buf, &nsize)) {
+               buf = convert_to_working_tree(ce->name, new, &size);
+               if (buf) {
                        free(new);
                        new = buf;
-                       size = nsize;
                }
 
                wrote = write_in_full(fd, new, size);
diff --git a/git.c b/git.c
index 7def319..f200907 100644 (file)
--- a/git.c
+++ b/git.c
@@ -234,6 +234,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp)
                { "cat-file", cmd_cat_file, RUN_SETUP },
                { "checkout-index", cmd_checkout_index, RUN_SETUP },
                { "check-ref-format", cmd_check_ref_format },
+               { "check-attr", cmd_check_attr, RUN_SETUP | NOT_BARE },
                { "cherry", cmd_cherry, RUN_SETUP },
                { "cherry-pick", cmd_cherry_pick, RUN_SETUP | NOT_BARE },
                { "commit-tree", cmd_commit_tree, RUN_SETUP },
index bed6b21..23db35a 100644 (file)
@@ -8,8 +8,11 @@ static const char *alternate_index_output;
 
 static void remove_lock_file(void)
 {
+       pid_t me = getpid();
+
        while (lock_file_list) {
-               if (lock_file_list->filename[0])
+               if (lock_file_list->owner == me &&
+                   lock_file_list->filename[0])
                        unlink(lock_file_list->filename);
                lock_file_list = lock_file_list->next;
        }
@@ -28,6 +31,7 @@ static int lock_file(struct lock_file *lk, const char *path)
        sprintf(lk->filename, "%s.lock", path);
        fd = open(lk->filename, O_RDWR | O_CREAT | O_EXCL, 0666);
        if (0 <= fd) {
+               lk->owner = getpid();
                if (!lk->on_list) {
                        lk->next = lock_file_list;
                        lock_file_list = lk;
index 31e66e5..403a4c8 100644 (file)
@@ -15,6 +15,8 @@
 #include "unpack-trees.h"
 #include "path-list.h"
 #include "xdiff-interface.h"
+#include "interpolate.h"
+#include "attr.h"
 
 static int subtree_merge;
 
@@ -645,6 +647,384 @@ static void fill_mm(const unsigned char *sha1, mmfile_t *mm)
        mm->size = size;
 }
 
+/*
+ * Customizable low-level merge drivers support.
+ */
+
+struct ll_merge_driver;
+typedef int (*ll_merge_fn)(const struct ll_merge_driver *,
+                          const char *path,
+                          mmfile_t *orig,
+                          mmfile_t *src1, const char *name1,
+                          mmfile_t *src2, const char *name2,
+                          mmbuffer_t *result);
+
+struct ll_merge_driver {
+       const char *name;
+       const char *description;
+       ll_merge_fn fn;
+       const char *recursive;
+       struct ll_merge_driver *next;
+       char *cmdline;
+};
+
+/*
+ * Built-in low-levels
+ */
+static int ll_xdl_merge(const struct ll_merge_driver *drv_unused,
+                       const char *path_unused,
+                       mmfile_t *orig,
+                       mmfile_t *src1, const char *name1,
+                       mmfile_t *src2, const char *name2,
+                       mmbuffer_t *result)
+{
+       xpparam_t xpp;
+
+       memset(&xpp, 0, sizeof(xpp));
+       return xdl_merge(orig,
+                        src1, name1,
+                        src2, name2,
+                        &xpp, XDL_MERGE_ZEALOUS,
+                        result);
+}
+
+static int ll_union_merge(const struct ll_merge_driver *drv_unused,
+                         const char *path_unused,
+                         mmfile_t *orig,
+                         mmfile_t *src1, const char *name1,
+                         mmfile_t *src2, const char *name2,
+                         mmbuffer_t *result)
+{
+       char *src, *dst;
+       long size;
+       const int marker_size = 7;
+
+       int status = ll_xdl_merge(drv_unused, path_unused,
+                                 orig, src1, NULL, src2, NULL, result);
+       if (status <= 0)
+               return status;
+       size = result->size;
+       src = dst = result->ptr;
+       while (size) {
+               char ch;
+               if ((marker_size < size) &&
+                   (*src == '<' || *src == '=' || *src == '>')) {
+                       int i;
+                       ch = *src;
+                       for (i = 0; i < marker_size; i++)
+                               if (src[i] != ch)
+                                       goto not_a_marker;
+                       if (src[marker_size] != '\n')
+                               goto not_a_marker;
+                       src += marker_size + 1;
+                       size -= marker_size + 1;
+                       continue;
+               }
+       not_a_marker:
+               do {
+                       ch = *src++;
+                       *dst++ = ch;
+                       size--;
+               } while (ch != '\n' && size);
+       }
+       result->size = dst - result->ptr;
+       return 0;
+}
+
+static int ll_binary_merge(const struct ll_merge_driver *drv_unused,
+                          const char *path_unused,
+                          mmfile_t *orig,
+                          mmfile_t *src1, const char *name1,
+                          mmfile_t *src2, const char *name2,
+                          mmbuffer_t *result)
+{
+       /*
+        * The tentative merge result is "ours" for the final round,
+        * or common ancestor for an internal merge.  Still return
+        * "conflicted merge" status.
+        */
+       mmfile_t *stolen = index_only ? orig : src1;
+
+       result->ptr = stolen->ptr;
+       result->size = stolen->size;
+       stolen->ptr = NULL;
+       return 1;
+}
+
+#define LL_BINARY_MERGE 0
+#define LL_TEXT_MERGE 1
+#define LL_UNION_MERGE 2
+static struct ll_merge_driver ll_merge_drv[] = {
+       { "binary", "built-in binary merge", ll_binary_merge },
+       { "text", "built-in 3-way text merge", ll_xdl_merge },
+       { "union", "built-in union merge", ll_union_merge },
+};
+
+static void create_temp(mmfile_t *src, char *path)
+{
+       int fd;
+
+       strcpy(path, ".merge_file_XXXXXX");
+       fd = mkstemp(path);
+       if (fd < 0)
+               die("unable to create temp-file");
+       if (write_in_full(fd, src->ptr, src->size) != src->size)
+               die("unable to write temp-file");
+       close(fd);
+}
+
+/*
+ * User defined low-level merge driver support.
+ */
+static int ll_ext_merge(const struct ll_merge_driver *fn,
+                       const char *path,
+                       mmfile_t *orig,
+                       mmfile_t *src1, const char *name1,
+                       mmfile_t *src2, const char *name2,
+                       mmbuffer_t *result)
+{
+       char temp[3][50];
+       char cmdbuf[2048];
+       struct interp table[] = {
+               { "%O" },
+               { "%A" },
+               { "%B" },
+       };
+       struct child_process child;
+       const char *args[20];
+       int status, fd, i;
+       struct stat st;
+
+       if (fn->cmdline == NULL)
+               die("custom merge driver %s lacks command line.", fn->name);
+
+       result->ptr = NULL;
+       result->size = 0;
+       create_temp(orig, temp[0]);
+       create_temp(src1, temp[1]);
+       create_temp(src2, temp[2]);
+
+       interp_set_entry(table, 0, temp[0]);
+       interp_set_entry(table, 1, temp[1]);
+       interp_set_entry(table, 2, temp[2]);
+
+       output(1, "merging %s using %s", path,
+              fn->description ? fn->description : fn->name);
+
+       interpolate(cmdbuf, sizeof(cmdbuf), fn->cmdline, table, 3);
+
+       memset(&child, 0, sizeof(child));
+       child.argv = args;
+       args[0] = "sh";
+       args[1] = "-c";
+       args[2] = cmdbuf;
+       args[3] = NULL;
+
+       status = run_command(&child);
+       if (status < -ERR_RUN_COMMAND_FORK)
+               ; /* failure in run-command */
+       else
+               status = -status;
+       fd = open(temp[1], O_RDONLY);
+       if (fd < 0)
+               goto bad;
+       if (fstat(fd, &st))
+               goto close_bad;
+       result->size = st.st_size;
+       result->ptr = xmalloc(result->size + 1);
+       if (read_in_full(fd, result->ptr, result->size) != result->size) {
+               free(result->ptr);
+               result->ptr = NULL;
+               result->size = 0;
+       }
+ close_bad:
+       close(fd);
+ bad:
+       for (i = 0; i < 3; i++)
+               unlink(temp[i]);
+       return status;
+}
+
+/*
+ * merge.default and merge.driver configuration items
+ */
+static struct ll_merge_driver *ll_user_merge, **ll_user_merge_tail;
+static const char *default_ll_merge;
+
+static int read_merge_config(const char *var, const char *value)
+{
+       struct ll_merge_driver *fn;
+       const char *ep, *name;
+       int namelen;
+
+       if (!strcmp(var, "merge.default")) {
+               if (value)
+                       default_ll_merge = strdup(value);
+               return 0;
+       }
+
+       /*
+        * We are not interested in anything but "merge.<name>.variable";
+        * especially, we do not want to look at variables such as
+        * "merge.summary", "merge.tool", and "merge.verbosity".
+        */
+       if (prefixcmp(var, "merge.") || (ep = strrchr(var, '.')) == var + 5)
+               return 0;
+
+       /*
+        * Find existing one as we might be processing merge.<name>.var2
+        * after seeing merge.<name>.var1.
+        */
+       name = var + 6;
+       namelen = ep - name;
+       for (fn = ll_user_merge; fn; fn = fn->next)
+               if (!strncmp(fn->name, name, namelen) && !fn->name[namelen])
+                       break;
+       if (!fn) {
+               char *namebuf;
+               fn = xcalloc(1, sizeof(struct ll_merge_driver));
+               namebuf = xmalloc(namelen + 1);
+               memcpy(namebuf, name, namelen);
+               namebuf[namelen] = 0;
+               fn->name = namebuf;
+               fn->fn = ll_ext_merge;
+               fn->next = NULL;
+               *ll_user_merge_tail = fn;
+               ll_user_merge_tail = &(fn->next);
+       }
+
+       ep++;
+
+       if (!strcmp("name", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               fn->description = strdup(value);
+               return 0;
+       }
+
+       if (!strcmp("driver", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               /*
+                * merge.<name>.driver specifies the command line:
+                *
+                *      command-line
+                *
+                * The command-line will be interpolated with the following
+                * tokens and is given to the shell:
+                *
+                *    %O - temporary file name for the merge base.
+                *    %A - temporary file name for our version.
+                *    %B - temporary file name for the other branches' version.
+                *
+                * The external merge driver should write the results in the
+                * file named by %A, and signal that it has done with zero exit
+                * status.
+                */
+               fn->cmdline = strdup(value);
+               return 0;
+       }
+
+       if (!strcmp("recursive", ep)) {
+               if (!value)
+                       return error("%s: lacks value", var);
+               fn->recursive = strdup(value);
+               return 0;
+       }
+
+       return 0;
+}
+
+static void initialize_ll_merge(void)
+{
+       if (ll_user_merge_tail)
+               return;
+       ll_user_merge_tail = &ll_user_merge;
+       git_config(read_merge_config);
+}
+
+static const struct ll_merge_driver *find_ll_merge_driver(const char *merge_attr)
+{
+       struct ll_merge_driver *fn;
+       const char *name;
+       int i;
+
+       initialize_ll_merge();
+
+       if (ATTR_TRUE(merge_attr))
+               return &ll_merge_drv[LL_TEXT_MERGE];
+       else if (ATTR_FALSE(merge_attr))
+               return &ll_merge_drv[LL_BINARY_MERGE];
+       else if (ATTR_UNSET(merge_attr)) {
+               if (!default_ll_merge)
+                       return &ll_merge_drv[LL_TEXT_MERGE];
+               else
+                       name = default_ll_merge;
+       }
+       else
+               name = merge_attr;
+
+       for (fn = ll_user_merge; fn; fn = fn->next)
+               if (!strcmp(fn->name, name))
+                       return fn;
+
+       for (i = 0; i < ARRAY_SIZE(ll_merge_drv); i++)
+               if (!strcmp(ll_merge_drv[i].name, name))
+                       return &ll_merge_drv[i];
+
+       /* default to the 3-way */
+       return &ll_merge_drv[LL_TEXT_MERGE];
+}
+
+static const char *git_path_check_merge(const char *path)
+{
+       static struct git_attr_check attr_merge_check;
+
+       if (!attr_merge_check.attr)
+               attr_merge_check.attr = git_attr("merge", 5);
+
+       if (git_checkattr(path, 1, &attr_merge_check))
+               return NULL;
+       return attr_merge_check.value;
+}
+
+static int ll_merge(mmbuffer_t *result_buf,
+                   struct diff_filespec *o,
+                   struct diff_filespec *a,
+                   struct diff_filespec *b,
+                   const char *branch1,
+                   const char *branch2)
+{
+       mmfile_t orig, src1, src2;
+       char *name1, *name2;
+       int merge_status;
+       const char *ll_driver_name;
+       const struct ll_merge_driver *driver;
+
+       name1 = xstrdup(mkpath("%s:%s", branch1, a->path));
+       name2 = xstrdup(mkpath("%s:%s", branch2, b->path));
+
+       fill_mm(o->sha1, &orig);
+       fill_mm(a->sha1, &src1);
+       fill_mm(b->sha1, &src2);
+
+       ll_driver_name = git_path_check_merge(a->path);
+       driver = find_ll_merge_driver(ll_driver_name);
+
+       if (index_only && driver->recursive)
+               driver = find_ll_merge_driver(driver->recursive);
+       merge_status = driver->fn(driver, a->path,
+                                 &orig, &src1, name1, &src2, name2,
+                                 result_buf);
+
+       free(name1);
+       free(name2);
+       free(orig.ptr);
+       free(src1.ptr);
+       free(src2.ptr);
+       return merge_status;
+}
+
 static struct merge_file_info merge_file(struct diff_filespec *o,
                struct diff_filespec *a, struct diff_filespec *b,
                const char *branch1, const char *branch2)
@@ -673,30 +1053,11 @@ static struct merge_file_info merge_file(struct diff_filespec *o,
                else if (sha_eq(b->sha1, o->sha1))
                        hashcpy(result.sha, a->sha1);
                else if (S_ISREG(a->mode)) {
-                       mmfile_t orig, src1, src2;
                        mmbuffer_t result_buf;
-                       xpparam_t xpp;
-                       char *name1, *name2;
                        int merge_status;
 
-                       name1 = xstrdup(mkpath("%s:%s", branch1, a->path));
-                       name2 = xstrdup(mkpath("%s:%s", branch2, b->path));
-
-                       fill_mm(o->sha1, &orig);
-                       fill_mm(a->sha1, &src1);
-                       fill_mm(b->sha1, &src2);
-
-                       memset(&xpp, 0, sizeof(xpp));
-                       merge_status = xdl_merge(&orig,
-                                                &src1, name1,
-                                                &src2, name2,
-                                                &xpp, XDL_MERGE_ZEALOUS,
-                                                &result_buf);
-                       free(name1);
-                       free(name2);
-                       free(orig.ptr);
-                       free(src1.ptr);
-                       free(src2.ptr);
+                       merge_status = ll_merge(&result_buf, o, a, b,
+                                               branch1, branch2);
 
                        if ((merge_status < 0) || !result_buf.ptr)
                                die("Failed to execute internal merge");
index 0c0fcc5..0642664 100644 (file)
@@ -2338,10 +2338,9 @@ int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object,
         */
        if ((type == OBJ_BLOB) && S_ISREG(st->st_mode)) {
                unsigned long nsize = size;
-               char *nbuf = buf;
-               if (convert_to_git(path, &nbuf, &nsize)) {
-                       if (size)
-                               munmap(buf, size);
+               char *nbuf = convert_to_git(path, buf, &nsize);
+               if (nbuf) {
+                       munmap(buf, size);
                        size = nsize;
                        buf = nbuf;
                        re_allocated = 1;
index 723b29a..fe1dfd0 100755 (executable)
@@ -4,6 +4,10 @@ test_description='CRLF conversion'
 
 . ./test-lib.sh
 
+q_to_nul () {
+       tr Q '\0'
+}
+
 append_cr () {
        sed -e 's/$/Q/' | tr Q '\015'
 }
@@ -20,6 +24,7 @@ test_expect_success setup '
        for w in Hello world how are you; do echo $w; done >one &&
        mkdir dir &&
        for w in I am very very fine thank you; do echo $w; done >dir/two &&
+       for w in Oh here is NULQin text here; do echo $w; done | q_to_nul >three &&
        git add . &&
 
        git commit -m initial &&
@@ -27,6 +32,7 @@ test_expect_success setup '
        one=`git rev-parse HEAD:one` &&
        dir=`git rev-parse HEAD:dir` &&
        two=`git rev-parse HEAD:dir/two` &&
+       three=`git rev-parse HEAD:three` &&
 
        for w in Some extra lines here; do echo $w; done >>one &&
        git diff >patch.file &&
@@ -38,7 +44,7 @@ test_expect_success setup '
 
 test_expect_success 'update with autocrlf=input' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git read-tree --reset -u HEAD &&
        git repo-config core.autocrlf input &&
 
@@ -62,7 +68,7 @@ test_expect_success 'update with autocrlf=input' '
 
 test_expect_success 'update with autocrlf=true' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git read-tree --reset -u HEAD &&
        git repo-config core.autocrlf true &&
 
@@ -86,7 +92,7 @@ test_expect_success 'update with autocrlf=true' '
 
 test_expect_success 'checkout with autocrlf=true' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf true &&
        git read-tree --reset -u HEAD &&
 
@@ -110,7 +116,7 @@ test_expect_success 'checkout with autocrlf=true' '
 
 test_expect_success 'checkout with autocrlf=input' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf input &&
        git read-tree --reset -u HEAD &&
 
@@ -136,7 +142,7 @@ test_expect_success 'checkout with autocrlf=input' '
 
 test_expect_success 'apply patch (autocrlf=input)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf input &&
        git read-tree --reset -u HEAD &&
 
@@ -149,7 +155,7 @@ test_expect_success 'apply patch (autocrlf=input)' '
 
 test_expect_success 'apply patch --cached (autocrlf=input)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf input &&
        git read-tree --reset -u HEAD &&
 
@@ -162,7 +168,7 @@ test_expect_success 'apply patch --cached (autocrlf=input)' '
 
 test_expect_success 'apply patch --index (autocrlf=input)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf input &&
        git read-tree --reset -u HEAD &&
 
@@ -176,7 +182,7 @@ test_expect_success 'apply patch --index (autocrlf=input)' '
 
 test_expect_success 'apply patch (autocrlf=true)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf true &&
        git read-tree --reset -u HEAD &&
 
@@ -189,7 +195,7 @@ test_expect_success 'apply patch (autocrlf=true)' '
 
 test_expect_success 'apply patch --cached (autocrlf=true)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf true &&
        git read-tree --reset -u HEAD &&
 
@@ -202,7 +208,7 @@ test_expect_success 'apply patch --cached (autocrlf=true)' '
 
 test_expect_success 'apply patch --index (autocrlf=true)' '
 
-       rm -f tmp one dir/two &&
+       rm -f tmp one dir/two three &&
        git repo-config core.autocrlf true &&
        git read-tree --reset -u HEAD &&
 
@@ -214,4 +220,74 @@ test_expect_success 'apply patch --index (autocrlf=true)' '
        }
 '
 
+test_expect_success '.gitattributes says two is binary' '
+
+       rm -f tmp one dir/two three &&
+       echo "two -crlf" >.gitattributes &&
+       git repo-config core.autocrlf true &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi &&
+
+       if remove_cr one >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi &&
+
+       if remove_cr three >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success '.gitattributes says two is input' '
+
+       rm -f tmp one dir/two three &&
+       echo "two crlf=input" >.gitattributes &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               echo "Huh?"
+               false
+       else
+               : happy
+       fi
+'
+
+test_expect_success '.gitattributes says two and three are text' '
+
+       rm -f tmp one dir/two three &&
+       echo "t* crlf" >.gitattributes &&
+       git read-tree --reset -u HEAD &&
+
+       if remove_cr dir/two >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi &&
+
+       if remove_cr three >/dev/null
+       then
+               : happy
+       else
+               echo "Huh?"
+               false
+       fi
+'
+
 test_done
diff --git a/t/t6026-merge-attr.sh b/t/t6026-merge-attr.sh
new file mode 100755 (executable)
index 0000000..56fc341
--- /dev/null
@@ -0,0 +1,145 @@
+#!/bin/sh
+#
+# Copyright (c) 2007 Junio C Hamano
+#
+
+test_description='per path merge controlled by merge attribute'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+
+       for f in text binary union
+       do
+               echo Initial >$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Initial &&
+
+       git branch side &&
+       for f in text binary union
+       do
+               echo Master >>$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Master &&
+
+       git checkout side &&
+       for f in text binary union
+       do
+               echo Side >>$f && git add $f || break
+       done &&
+       test_tick &&
+       git commit -m Side &&
+
+       git tag anchor
+'
+
+test_expect_success merge '
+
+       {
+               echo "binary -merge"
+               echo "union merge=union"
+       } >.gitattributes &&
+
+       if git merge master
+       then
+               echo Gaah, should have conflicted
+               false
+       else
+               echo Ok, conflicted.
+       fi
+'
+
+test_expect_success 'check merge result in index' '
+
+       git ls-files -u | grep binary &&
+       git ls-files -u | grep text &&
+       ! (git ls-files -u | grep union)
+
+'
+
+test_expect_success 'check merge result in working tree' '
+
+       git cat-file -p HEAD:binary >binary-orig &&
+       grep "<<<<<<<" text &&
+       cmp binary-orig binary &&
+       ! grep "<<<<<<<" union &&
+       grep Master union &&
+       grep Side union
+
+'
+
+cat >./custom-merge <<\EOF
+#!/bin/sh
+
+orig="$1" ours="$2" theirs="$3" exit="$4"
+(
+       echo "orig is $orig"
+       echo "ours is $ours"
+       echo "theirs is $theirs"
+       echo "=== orig ==="
+       cat "$orig"
+       echo "=== ours ==="
+       cat "$ours"
+       echo "=== theirs ==="
+       cat "$theirs"
+) >"$ours+"
+cat "$ours+" >"$ours"
+rm -f "$ours+"
+exit "$exit"
+EOF
+chmod +x ./custom-merge
+
+test_expect_success 'custom merge backend' '
+
+       echo "* merge=union" >.gitattributes &&
+       echo "text merge=custom" >>.gitattributes &&
+
+       git reset --hard anchor &&
+       git config --replace-all \
+       merge.custom.driver "./custom-merge %O %A %B 0" &&
+       git config --replace-all \
+       merge.custom.name "custom merge driver for testing" &&
+
+       git merge master &&
+
+       cmp binary union &&
+       sed -e 1,3d text >check-1 &&
+       o=$(git-unpack-file master^:text) &&
+       a=$(git-unpack-file side^:text) &&
+       b=$(git-unpack-file master:text) &&
+       sh -c "./custom-merge $o $a $b 0" &&
+       sed -e 1,3d $a >check-2 &&
+       cmp check-1 check-2 &&
+       rm -f $o $a $b
+'
+
+test_expect_success 'custom merge backend' '
+
+       git reset --hard anchor &&
+       git config --replace-all \
+       merge.custom.driver "./custom-merge %O %A %B 1" &&
+       git config --replace-all \
+       merge.custom.name "custom merge driver for testing" &&
+
+       if git merge master
+       then
+               echo "Eh? should have conflicted"
+               false
+       else
+               echo "Ok, conflicted"
+       fi &&
+
+       cmp binary union &&
+       sed -e 1,3d text >check-1 &&
+       o=$(git-unpack-file master^:text) &&
+       a=$(git-unpack-file anchor:text) &&
+       b=$(git-unpack-file master:text) &&
+       sh -c "./custom-merge $o $a $b 0" &&
+       sed -e 1,3d $a >check-2 &&
+       cmp check-1 check-2 &&
+       rm -f $o $a $b
+'
+
+test_done