diff --git a/Documentation/Makefile b/Documentation/Makefile index 7a8037f586..06b0c57b95 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -84,7 +84,7 @@ endif # ifdef ASCIIDOC8 -ASCIIDOC_EXTRA += -a asciidoc7compatible +ASCIIDOC_EXTRA += -a asciidoc7compatible -a no-inline-literal endif ifdef DOCBOOK_XSL_172 ASCIIDOC_EXTRA += -a git-asciidoc-no-roff diff --git a/Documentation/RelNotes-1.6.3.4.txt b/Documentation/RelNotes-1.6.3.4.txt new file mode 100644 index 0000000000..cad461bc76 --- /dev/null +++ b/Documentation/RelNotes-1.6.3.4.txt @@ -0,0 +1,36 @@ +GIT v1.6.3.4 Release Notes +========================== + +Fixes since v1.6.3.3 +-------------------- + + * "git add --no-ignore-errors" did not override configured + add.ignore-errors configuration. + + * "git apply --whitespace=fix" did not fix trailing whitespace on an + incomplete line. + + * "git branch" opened too many commit objects unnecessarily. + + * "git checkout -f $commit" with a path that is a file (or a symlink) in + the work tree to a commit that has a directory at the path issued an + unnecessary error message. + + * "git diff -c/--cc" was very inefficient in coalescing the removed lines + shared between parents. + + * "git diff -c/--cc" showed removed lines at the beginning of a file + incorrectly. + + * "git remote show nickname" did not honor configured + remote.nickname.uploadpack when inspecting the branches at the remote. + + * "git request-pull" when talking to the terminal for a preview + showed some of the output in the pager. + + * "git request-pull start nickname [end]" did not honor configured + remote.nickname.uploadpack when it ran git-ls-remote against the remote + repository to learn the current tip of branches. + +Includes other documentation updates and minor fixes. + diff --git a/Documentation/RelNotes-1.6.4.txt b/Documentation/RelNotes-1.6.4.txt index f578b8186f..7a904419f7 100644 --- a/Documentation/RelNotes-1.6.4.txt +++ b/Documentation/RelNotes-1.6.4.txt @@ -32,13 +32,14 @@ Updates since v1.6.3 * git-svn updates, including a new --authors-prog option to map author names by invoking an external program, 'git svn reset' to unwind - 'git svn fetch', support for more than one branches, etc. + 'git svn fetch', support for more than one branches, documenting + of the useful --minimize-url feature, new "git svn gc" command, etc. (portability) * We feed iconv with "UTF-8" instead of "utf8"; the former is understood more widely. Similarly updated test scripts to use - encoding names more widely understood (e.g. use "ISO8850-1" instead + encoding names more widely understood (e.g. use "ISO8859-1" instead of "ISO-8859-1"). * Various portability fixes/workarounds for different vintages of @@ -66,6 +67,12 @@ Updates since v1.6.3 * "git cvsexportcommit" learned -k option to stop CVS keywords expansion + * "git fast-export" learned to handle history simplification more + gracefully. + + * "git fast-export" learned an option --tag-of-filtered-object to handle + dangling tags resulting from history simplification more usefully. + * "git grep" learned -p option to show the location of the match using the same context hunk marker "git diff" uses. @@ -75,7 +82,7 @@ Updates since v1.6.3 * "git imap-send" is IPv6 aware. - * "git log --graph" draws graphs more compactly by using horizonal lines + * "git log --graph" draws graphs more compactly by using horizontal lines when able. * "git log --decorate" shows shorter refnames by stripping well-known @@ -105,6 +112,8 @@ Updates since v1.6.3 * A major part of the "git bisect" wrapper has moved to C. + * Formatting with the new version of AsciiDoc 8.4.1 is now supported. + Fixes since v1.6.3 ------------------ @@ -123,8 +132,16 @@ v1.6.3.X series. that chdir around. It now internally records the repository location as an absolute path when autodetected. ---- -exec >/var/tmp/1 -echo O=$(git describe master) -O=v1.6.4-rc1-7-gbba0fd2 -git shortlog --no-merges $O..master ^maint + * Removing a section with "git config --remove-section", when its + section header has a variable definition on the same line, lost + that variable definition. + + * "git rebase -p --onto" used to always leave side branches of a merge + intact, even when both branches are subject to rewriting. + + * "git repack" used to faithfully follow grafts and considered true + parents recorded in the commit object unreachable from the commit. + After such a repacking, you cannot remove grafts without corrupting + the repository. + + * "git send-email" did not detect erroneous loops in alias expansion. diff --git a/Documentation/config.txt b/Documentation/config.txt index 6857d2f4a9..c6f09f801a 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -49,7 +49,8 @@ There is also a case insensitive alternative `[section.subsection]` syntax. In this syntax, subsection names follow the same restrictions as for section names. -All the other lines are recognized as setting variables, in the form +All the other lines (and the remainder of the line after the section +header) are recognized as setting variables, in the form 'name = value'. If there is no equal sign on the line, the entire line is taken as 'name' and the variable is recognized as boolean "true". The variable names are case-insensitive and only alphanumeric diff --git a/Documentation/diff-format.txt b/Documentation/diff-format.txt index 1eeb1c7683..b71712473e 100644 --- a/Documentation/diff-format.txt +++ b/Documentation/diff-format.txt @@ -1,4 +1,7 @@ -The output format from "git-diff-index", "git-diff-tree", +Raw output format +----------------- + +The raw output format from "git-diff-index", "git-diff-tree", "git-diff-files" and "git diff --raw" are very similar. These commands all compare two sets of things; what is @@ -16,6 +19,9 @@ git-diff-tree [-r] [...]:: git-diff-files [...]:: compares the index and the files on the filesystem. +The "git-diff-tree" command begins its ouput by printing the hash of +what is being compared. After that, all the commands print one output +line per changed file. An output line is formatted this way: diff --git a/Documentation/git-diff-files.txt b/Documentation/git-diff-files.txt index c526141564..4ef03578eb 100644 --- a/Documentation/git-diff-files.txt +++ b/Documentation/git-diff-files.txt @@ -43,8 +43,7 @@ omit diff output for unmerged entries and just show "Unmerged". -q:: Remain silent even on nonexistent files -Output format -------------- + include::diff-format.txt[] diff --git a/Documentation/git-diff-index.txt b/Documentation/git-diff-index.txt index 26920d4f63..8b9ed29299 100644 --- a/Documentation/git-diff-index.txt +++ b/Documentation/git-diff-index.txt @@ -34,8 +34,6 @@ include::diff-options.txt[] 'git-diff-index' say that all non-checked-out files are up to date. -Output format -------------- include::diff-format.txt[] Operating Modes diff --git a/Documentation/git-diff-tree.txt b/Documentation/git-diff-tree.txt index 23b7abd3c6..f2cef1260b 100644 --- a/Documentation/git-diff-tree.txt +++ b/Documentation/git-diff-tree.txt @@ -159,8 +159,7 @@ HEAD commits it finds, which is even more interesting. in case you care). -Output format -------------- + include::diff-format.txt[] diff --git a/Documentation/git-diff.txt b/Documentation/git-diff.txt index a2f192fb75..0ac711230e 100644 --- a/Documentation/git-diff.txt +++ b/Documentation/git-diff.txt @@ -84,8 +84,7 @@ include::diff-options.txt[] the diff to the named paths (you can give directory names and get diff for all files under them). -Output format -------------- + include::diff-format.txt[] EXAMPLES diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt index 0c9eb567cb..af2328d401 100644 --- a/Documentation/git-fast-export.txt +++ b/Documentation/git-fast-export.txt @@ -36,6 +36,17 @@ when encountering a signed tag. With 'strip', the tags will be made unsigned, with 'verbatim', they will be silently exported and with 'warn', they will be exported, but you will see a warning. +--tag-of-filtered-object=(abort|drop|rewrite):: + Specify how to handle tags whose tagged objectis filtered out. + Since revisions and files to export can be limited by path, + tagged objects may be filtered completely. ++ +When asking to 'abort' (which is the default), this program will die +when encountering such a tag. With 'drop' it will omit such tags from +the output. With 'rewrite', if the tagged object is a commit, it will +rewrite the tag to tag an ancestor commit (via parent rewriting; see +linkgit:git-rev-list[1]) + -M:: -C:: Perform move and/or copy detection, as described in the @@ -71,6 +82,12 @@ marks the same across runs. allow that. So fake a tagger to be able to fast-import the output. +[git-rev-list-args...]:: + A list of arguments, acceptable to 'git-rev-parse' and + 'git-rev-list', that specifies the specific objects and references + to export. For example, `master\~10..master` causes the + current master reference to be exported along with all objects + added since its 10th ancestor commit. EXAMPLES -------- diff --git a/Documentation/git-pack-objects.txt b/Documentation/git-pack-objects.txt index 7d4c1a7556..2e4992970e 100644 --- a/Documentation/git-pack-objects.txt +++ b/Documentation/git-pack-objects.txt @@ -11,7 +11,8 @@ SYNOPSIS [verse] 'git pack-objects' [-q] [--no-reuse-delta] [--delta-base-offset] [--non-empty] [--local] [--incremental] [--window=N] [--depth=N] [--all-progress] - [--revs [--unpacked | --all]*] [--stdout | base-name] < object-list + [--revs [--unpacked | --all]*] [--stdout | base-name] + [--keep-true-parents] < object-list DESCRIPTION @@ -197,6 +198,10 @@ base-name:: to force the version for the generated pack index, and to force 64-bit index entries on objects located above the given offset. +--keep-true-parents:: + With this option, parents that are hidden by grafts are packed + nevertheless. + Author ------ diff --git a/Documentation/git-rerere.txt b/Documentation/git-rerere.txt index a53c3cd35b..7dd515b8cc 100644 --- a/Documentation/git-rerere.txt +++ b/Documentation/git-rerere.txt @@ -23,7 +23,7 @@ on the initial manual merge, and applying previously recorded hand resolutions to their corresponding automerge results. [NOTE] -You need to set the configuration variable rerere.enabled to +You need to set the configuration variable rerere.enabled in order to enable this command. diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index 10af599b44..22a0389f1e 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -80,6 +80,17 @@ COMMANDS When passed to 'init' or 'clone' this regular expression will be preserved as a config key. See 'fetch' for a description of '--ignore-paths'. +--no-minimize-url;; + When tracking multiple directories (using --stdlayout, + --branches, or --tags options), git svn will attempt to connect + to the root (or highest allowed level) of the Subversion + repository. This default allows better tracking of history if + entire projects are moved within a repository, but may cause + issues on repositories where read access restrictions are in + place. Passing '--no-minimize-url' will allow git svn to + accept URLs as-is without attempting to connect to a higher + level directory. This option is off by default when only + one URL/branch is tracked (it would do little good). 'fetch':: Fetch unfetched revisions from the Subversion remote we are @@ -338,6 +349,10 @@ Any other arguments are passed directly to 'git log' Shows the Subversion externals. Use -r/--revision to specify a specific revision. +'gc':: + Compress $GIT_DIR/svn//unhandled.log files in .git/svn + and remove $GIT_DIR/svn/index files in .git/svn. + 'reset':: Undoes the effects of 'fetch' back to the specified revision. This allows you to re-'fetch' an SVN revision. Normally the diff --git a/Documentation/git.txt b/Documentation/git.txt index 6fa0310e05..5fd5953e29 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -43,9 +43,15 @@ unreleased) version of git, that is available from 'master' branch of the `git.git` repository. Documentation for older releases are available here: -* link:v1.6.3.3/git.html[documentation for release 1.6.3.3] +* link:v1.6.4/git.html[documentation for release 1.6.4] * release notes for + link:RelNotes-1.6.4.txt[1.6.4]. + +* link:v1.6.3.4/git.html[documentation for release 1.6.3.4] + +* release notes for + link:RelNotes-1.6.3.4.txt[1.6.3.4], link:RelNotes-1.6.3.3.txt[1.6.3.3], link:RelNotes-1.6.3.2.txt[1.6.3.2], link:RelNotes-1.6.3.1.txt[1.6.3.1], diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index 39cde784c9..d8ae315140 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -1,7 +1,7 @@ #!/bin/sh GVF=GIT-VERSION-FILE -DEF_VER=v1.6.3.GIT +DEF_VER=v1.6.4 LF=' ' diff --git a/Makefile b/Makefile index bde27ed478..daf4296706 100644 --- a/Makefile +++ b/Makefile @@ -728,6 +728,7 @@ ifeq ($(uname_S),SunOS) NO_MKDTEMP = YesPlease NO_MKSTEMPS = YesPlease NO_REGEX = YesPlease + NO_EXTERNAL_GREP = YesPlease ifeq ($(uname_R),5.7) NEEDS_RESOLV = YesPlease NO_IPV6 = YesPlease diff --git a/builtin-branch.c b/builtin-branch.c index 5687d6042c..1a03d5f356 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -191,7 +191,7 @@ struct ref_item { struct ref_list { struct rev_info revs; - int index, alloc, maxwidth; + int index, alloc, maxwidth, verbose, abbrev; struct ref_item *list; struct commit_list *with_commit; int kinds; @@ -240,21 +240,24 @@ static int append_ref(const char *refname, const unsigned char *sha1, int flags, if (ARRAY_SIZE(ref_kind) <= i) return 0; - commit = lookup_commit_reference_gently(sha1, 1); - if (!commit) - return error("branch '%s' does not point at a commit", refname); - - /* Filter with with_commit if specified */ - if (!is_descendant_of(commit, ref_list->with_commit)) - return 0; - /* Don't add types the caller doesn't want */ if ((kind & ref_list->kinds) == 0) return 0; - if (merge_filter != NO_FILTER) - add_pending_object(&ref_list->revs, - (struct object *)commit, refname); + commit = NULL; + if (ref_list->verbose || ref_list->with_commit || merge_filter != NO_FILTER) { + commit = lookup_commit_reference_gently(sha1, 1); + if (!commit) + return error("branch '%s' does not point at a commit", refname); + + /* Filter with with_commit if specified */ + if (!is_descendant_of(commit, ref_list->with_commit)) + return 0; + + if (merge_filter != NO_FILTER) + add_pending_object(&ref_list->revs, + (struct object *)commit, refname); + } /* Resize buffer */ if (ref_list->index >= ref_list->alloc) { @@ -415,18 +418,38 @@ static int calc_maxwidth(struct ref_list *refs) return w; } + +static void show_detached(struct ref_list *ref_list) +{ + struct commit *head_commit = lookup_commit_reference_gently(head_sha1, 1); + + if (head_commit && is_descendant_of(head_commit, ref_list->with_commit)) { + struct ref_item item; + item.name = xstrdup("(no branch)"); + item.len = strlen(item.name); + item.kind = REF_LOCAL_BRANCH; + item.dest = NULL; + item.commit = head_commit; + if (item.len > ref_list->maxwidth) + ref_list->maxwidth = item.len; + print_ref_item(&item, ref_list->maxwidth, ref_list->verbose, ref_list->abbrev, 1, ""); + free(item.name); + } +} + static void print_ref_list(int kinds, int detached, int verbose, int abbrev, struct commit_list *with_commit) { int i; struct ref_list ref_list; - struct commit *head_commit = lookup_commit_reference_gently(head_sha1, 1); memset(&ref_list, 0, sizeof(ref_list)); ref_list.kinds = kinds; + ref_list.verbose = verbose; + ref_list.abbrev = abbrev; ref_list.with_commit = with_commit; if (merge_filter != NO_FILTER) init_revisions(&ref_list.revs, NULL); - for_each_ref(append_ref, &ref_list); + for_each_rawref(append_ref, &ref_list); if (merge_filter != NO_FILTER) { struct commit *filter; filter = lookup_commit_reference_gently(merge_filter_ref, 0); @@ -442,19 +465,8 @@ static void print_ref_list(int kinds, int detached, int verbose, int abbrev, str qsort(ref_list.list, ref_list.index, sizeof(struct ref_item), ref_cmp); detached = (detached && (kinds & REF_LOCAL_BRANCH)); - if (detached && head_commit && - is_descendant_of(head_commit, with_commit)) { - struct ref_item item; - item.name = xstrdup("(no branch)"); - item.len = strlen(item.name); - item.kind = REF_LOCAL_BRANCH; - item.dest = NULL; - item.commit = head_commit; - if (item.len > ref_list.maxwidth) - ref_list.maxwidth = item.len; - print_ref_item(&item, ref_list.maxwidth, verbose, abbrev, 1, ""); - free(item.name); - } + if (detached) + show_detached(&ref_list); for (i = 0; i < ref_list.index; i++) { int current = !detached && diff --git a/builtin-fast-export.c b/builtin-fast-export.c index 9a8a6fc6b1..c48c18d0c8 100644 --- a/builtin-fast-export.c +++ b/builtin-fast-export.c @@ -23,7 +23,8 @@ static const char *fast_export_usage[] = { }; static int progress; -static enum { VERBATIM, WARN, STRIP, ABORT } signed_tag_mode = ABORT; +static enum { ABORT, VERBATIM, WARN, STRIP } signed_tag_mode = ABORT; +static enum { ERROR, DROP, REWRITE } tag_of_filtered_mode = ABORT; static int fake_missing_tagger; static int parse_opt_signed_tag_mode(const struct option *opt, @@ -42,6 +43,20 @@ static int parse_opt_signed_tag_mode(const struct option *opt, return 0; } +static int parse_opt_tag_of_filtered_mode(const struct option *opt, + const char *arg, int unset) +{ + if (unset || !strcmp(arg, "abort")) + tag_of_filtered_mode = ABORT; + else if (!strcmp(arg, "drop")) + tag_of_filtered_mode = DROP; + else if (!strcmp(arg, "rewrite")) + tag_of_filtered_mode = REWRITE; + else + return error("Unknown tag-of-filtered mode: %s", arg); + return 0; +} + static struct decoration idnums; static uint32_t last_idnum; @@ -289,6 +304,23 @@ static void handle_tag(const char *name, struct tag *tag) char *buf; const char *tagger, *tagger_end, *message; size_t message_size = 0; + struct object *tagged; + int tagged_mark; + struct commit *p; + + /* Trees have no identifer in fast-export output, thus we have no way + * to output tags of trees, tags of tags of trees, etc. Simply omit + * such tags. + */ + tagged = tag->tagged; + while (tagged->type == OBJ_TAG) { + tagged = ((struct tag *)tagged)->tagged; + } + if (tagged->type == OBJ_TREE) { + warning("Omitting tag %s,\nsince tags of trees (or tags of tags of trees, etc.) are not supported.", + sha1_to_hex(tag->object.sha1)); + return; + } buf = read_sha1_file(tag->object.sha1, &type, &size); if (!buf) @@ -333,10 +365,45 @@ static void handle_tag(const char *name, struct tag *tag) } } + /* handle tag->tagged having been filtered out due to paths specified */ + tagged = tag->tagged; + tagged_mark = get_object_mark(tagged); + if (!tagged_mark) { + switch(tag_of_filtered_mode) { + case ABORT: + die ("Tag %s tags unexported object; use " + "--tag-of-filtered-object= to handle it.", + sha1_to_hex(tag->object.sha1)); + case DROP: + /* Ignore this tag altogether */ + return; + case REWRITE: + if (tagged->type != OBJ_COMMIT) { + die ("Tag %s tags unexported %s!", + sha1_to_hex(tag->object.sha1), + typename(tagged->type)); + } + p = (struct commit *)tagged; + for (;;) { + if (p->parents && p->parents->next) + break; + if (p->object.flags & UNINTERESTING) + break; + if (!(p->object.flags & TREESAME)) + break; + if (!p->parents) + die ("Can't find replacement commit for tag %s\n", + sha1_to_hex(tag->object.sha1)); + p = p->parents->item; + } + tagged_mark = get_object_mark(&p->object); + } + } + if (!prefixcmp(name, "refs/tags/")) name += 10; printf("tag %s\nfrom :%d\n%.*s%sdata %d\n%.*s\n", - name, get_object_mark(tag->tagged), + name, tagged_mark, (int)(tagger_end - tagger), tagger, tagger == tagger_end ? "" : "\n", (int)message_size, (int)message_size, message ? message : ""); @@ -428,21 +495,27 @@ static void export_marks(char *file) uint32_t mark; struct object_decoration *deco = idnums.hash; FILE *f; + int e = 0; f = fopen(file, "w"); if (!f) - error("Unable to open marks file %s for writing", file); + error("Unable to open marks file %s for writing.", file); for (i = 0; i < idnums.size; i++) { if (deco->base && deco->base->type == 1) { mark = ptr_to_mark(deco->decoration); - fprintf(f, ":%"PRIu32" %s\n", mark, - sha1_to_hex(deco->base->sha1)); + if (fprintf(f, ":%"PRIu32" %s\n", mark, + sha1_to_hex(deco->base->sha1)) < 0) { + e = 1; + break; + } } deco++; } - if (ferror(f) || fclose(f)) + e |= ferror(f); + e |= fclose(f); + if (e) error("Unable to write marks file %s.", file); } @@ -498,6 +571,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) OPT_CALLBACK(0, "signed-tags", &signed_tag_mode, "mode", "select handling of signed tags", parse_opt_signed_tag_mode), + OPT_CALLBACK(0, "tag-of-filtered-object", &tag_of_filtered_mode, "mode", + "select handling of tags that tag filtered objects", + parse_opt_tag_of_filtered_mode), OPT_STRING(0, "export-marks", &export_filename, "FILE", "Dump marks to this file"), OPT_STRING(0, "import-marks", &import_filename, "FILE", @@ -514,6 +590,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) git_config(git_default_config, NULL); init_revisions(&revs, prefix); + revs.topo_order = 1; + revs.show_source = 1; + revs.rewrite_parents = 1; argc = setup_revisions(argc, argv, &revs, NULL); argc = parse_options(argc, argv, prefix, options, fast_export_usage, 0); if (argc > 1) @@ -524,18 +603,13 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) get_tags_and_duplicates(&revs.pending, &extra_refs); - revs.topo_order = 1; if (prepare_revision_walk(&revs)) die("revision walk setup failed"); revs.diffopt.format_callback = show_filemodify; DIFF_OPT_SET(&revs.diffopt, RECURSIVE); while ((commit = get_revision(&revs))) { if (has_unshown_parent(commit)) { - struct commit_list *parent = commit->parents; add_object_array(&commit->object, NULL, &commits); - for (; parent; parent = parent->next) - if (!parent->item->util) - parent->item->util = commit->util; } else { handle_commit(commit, &revs); diff --git a/builtin-pack-objects.c b/builtin-pack-objects.c index a27c2f6277..ef4bf6bc14 100644 --- a/builtin-pack-objects.c +++ b/builtin-pack-objects.c @@ -2255,6 +2255,10 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) die("bad %s", arg); continue; } + if (!strcmp(arg, "--keep-true-parents")) { + grafts_replace_parents = 0; + continue; + } usage(pack_usage); } diff --git a/cache.h b/cache.h index c72f125bd4..e6c7f3307d 100644 --- a/cache.h +++ b/cache.h @@ -560,6 +560,8 @@ enum object_creation_mode { extern enum object_creation_mode object_creation_mode; +extern int grafts_replace_parents; + #define GIT_REPO_VERSION 0 extern int repository_format_version; extern int check_repository_format(void); diff --git a/commit.c b/commit.c index a47fb4da27..e2bcbe8149 100644 --- a/commit.c +++ b/commit.c @@ -262,7 +262,11 @@ int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size) bufptr[47] != '\n') return error("bad parents in commit %s", sha1_to_hex(item->object.sha1)); bufptr += 48; - if (graft) + /* + * The clone is shallow if nr_parent < 0, and we must + * not traverse its real parents even when we unhide them. + */ + if (graft && (graft->nr_parent < 0 || grafts_replace_parents)) continue; new_parent = lookup_commit(parent); if (new_parent) diff --git a/config.c b/config.c index 1682273c12..738b24419d 100644 --- a/config.c +++ b/config.c @@ -1174,7 +1174,9 @@ write_err_out: static int section_name_match (const char *buf, const char *name) { int i = 0, j = 0, dot = 0; - for (; buf[i] && buf[i] != ']'; i++) { + if (buf[i] != '[') + return 0; + for (i = 1; buf[i] && buf[i] != ']'; i++) { if (!dot && isspace(buf[i])) { dot = 1; if (name[j++] != '.') @@ -1195,7 +1197,17 @@ static int section_name_match (const char *buf, const char *name) if (buf[i] != name[j++]) break; } - return (buf[i] == ']' && name[j] == 0); + if (buf[i] == ']' && name[j] == 0) { + /* + * We match, now just find the right length offset by + * gobbling up any whitespace after it, as well + */ + i++; + for (; buf[i] && isspace(buf[i]); i++) + ; /* do nothing */ + return i; + } + return 0; } /* if new_name == NULL, the section is removed instead */ @@ -1225,11 +1237,13 @@ int git_config_rename_section(const char *old_name, const char *new_name) while (fgets(buf, sizeof(buf), config_file)) { int i; int length; + char *output = buf; for (i = 0; buf[i] && isspace(buf[i]); i++) ; /* do nothing */ if (buf[i] == '[') { /* it's a section */ - if (section_name_match (&buf[i+1], old_name)) { + int offset = section_name_match(&buf[i], old_name); + if (offset > 0) { ret++; if (new_name == NULL) { remove = 1; @@ -1240,14 +1254,29 @@ int git_config_rename_section(const char *old_name, const char *new_name) ret = write_error(lock->filename); goto out; } - continue; + /* + * We wrote out the new section, with + * a newline, now skip the old + * section's length + */ + output += offset + i; + if (strlen(output) > 0) { + /* + * More content means there's + * a declaration to put on the + * next line; indent with a + * tab + */ + output -= 1; + output[0] = '\t'; + } } remove = 0; } if (remove) continue; - length = strlen(buf); - if (write_in_full(out_fd, buf, length) != length) { + length = strlen(output); + if (write_in_full(out_fd, output, length) != length) { ret = write_error(lock->filename); goto out; } diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 887731e830..745b5fb78b 100755 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -44,6 +44,10 @@ # GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed, # then a '$' will be shown next to the branch name. # +# If you would like to see if there're untracked files, then you can +# set GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're +# untracked files, then a '%' will be shown next to the branch name. +# # To submit patches: # # *) Read Documentation/SubmittingPatches @@ -132,6 +136,7 @@ __git_ps1 () local w local i local s + local u local c if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then @@ -156,12 +161,18 @@ __git_ps1 () if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$" fi + + if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then + if [ -n "$(git ls-files --others --exclude-standard)" ]; then + u="%" + fi + fi fi if [ -n "${1-}" ]; then - printf "$1" "$c${b##refs/heads/}$w$i$s$r" + printf "$1" "$c${b##refs/heads/}$w$i$s$u$r" else - printf " (%s)" "$c${b##refs/heads/}$w$i$s$r" + printf " (%s)" "$c${b##refs/heads/}$w$i$s$u$r" fi fi } diff --git a/environment.c b/environment.c index 720f26b67d..8f5eaa7dd8 100644 --- a/environment.c +++ b/environment.c @@ -47,6 +47,7 @@ enum push_default_type push_default = PUSH_DEFAULT_MATCHING; #define OBJECT_CREATION_MODE OBJECT_CREATION_USES_HARDLINKS #endif enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE; +int grafts_replace_parents = 1; /* Parallel index stat data preload? */ int core_preload_index = 0; diff --git a/git-instaweb.sh b/git-instaweb.sh index 5f4419b69b..32f6496b0d 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -256,7 +256,7 @@ apache2_conf () { mkdir -p "$GIT_DIR/gitweb/logs" bind= test x"$local" = xtrue && bind='127.0.0.1:' - echo 'text/css css' > $fqgitdir/mime.types + echo 'text/css css' > "$fqgitdir/mime.types" cat > "$conf" <> "$conf" <&2 git log --max-count=1 --pretty='tformat:warn: %h: %s' $headrev >&2 diff --git a/git-send-email.perl b/git-send-email.perl index 8ce6f1fe57..d508f83349 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -654,13 +654,17 @@ if (!@to) { } sub expand_aliases { - my @cur = @_; - my @last; - do { - @last = @cur; - @cur = map { $aliases{$_} ? @{$aliases{$_}} : $_ } @last; - } while (join(',',@cur) ne join(',',@last)); - return @cur; + return map { expand_one_alias($_) } @_; +} + +my %EXPANDED_ALIASES; +sub expand_one_alias { + my $alias = shift; + if ($EXPANDED_ALIASES{$alias}) { + die "fatal: alias '$alias' expands to itself\n"; + } + local $EXPANDED_ALIASES{$alias} = 1; + return $aliases{$alias} ? expand_aliases(@{$aliases{$alias}}) : $alias; } @to = expand_aliases(@to); diff --git a/git-svn.perl b/git-svn.perl index 43c86e85a1..d075810724 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -19,6 +19,7 @@ $ENV{GIT_DIR} ||= '.git'; $Git::SVN::default_repo_id = 'svn'; $Git::SVN::default_ref_id = $ENV{GIT_SVN_ID} || 'git-svn'; $Git::SVN::Ra::_log_window_size = 100; +$Git::SVN::_minimize_url = 'unset'; $Git::SVN::Log::TZ = $ENV{TZ}; $ENV{TZ} = 'UTC'; @@ -31,6 +32,7 @@ require SVN::Delta; if ($SVN::Core::VERSION lt '1.1.0') { fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)"; } +my $can_compress = eval { require Compress::Zlib; 1}; push @Git::SVN::Ra::ISA, 'SVN::Ra'; push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor'; @@ -40,6 +42,7 @@ use IO::File qw//; use File::Basename qw/dirname basename/; use File::Path qw/mkpath/; use File::Spec; +use File::Find; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; use IPC::Open3; use Git; @@ -98,7 +101,7 @@ my %init_opts = ( 'template=s' => \$_template, 'shared:s' => \$_shared, 'trunk|T=s' => \$_trunk, 'tags|t=s@' => \@_tags, 'branches|b=s@' => \@_branches, 'prefix=s' => \$_prefix, 'stdlayout|s' => \$_stdlayout, - 'minimize-url|m' => \$Git::SVN::_minimize_url, + 'minimize-url|m!' => \$Git::SVN::_minimize_url, 'no-metadata' => sub { $icv{noMetadata} = 1 }, 'use-svm-props' => sub { $icv{useSvmProps} = 1 }, 'use-svnsync-props' => sub { $icv{useSvnsyncProps} = 1 }, @@ -217,6 +220,10 @@ my %cmd = ( "Undo fetches back to the specified SVN revision", { 'revision|r=s' => \$_revision, 'parent|p' => \$_fetch_parent } ], + 'gc' => [ \&cmd_gc, + "Compress unhandled.log files in .git/svn and remove " . + "index files in .git/svn", + {} ], ); my $cmd; @@ -393,6 +400,10 @@ sub cmd_init { init_subdir(@_); do_git_init_db(); + if ($Git::SVN::_minimize_url eq 'unset') { + $Git::SVN::_minimize_url = 0; + } + Git::SVN->init($url); } @@ -655,9 +666,22 @@ sub cmd_branch { } } unless (defined $glob) { - die "Unknown ", - $_tag ? "tag" : "branch", - " destination $_branch_dest\n"; + my $dest_re = qr/\b\Q$_branch_dest\E\b/; + foreach my $g (@{$allglobs}) { + $g->{path}->{left} =~ /$dest_re/ or next; + if (defined $glob) { + die "Ambiguous destination: ", + $_branch_dest, "\nmatches both '", + $glob->{path}->{left}, "' and '", + $g->{path}->{left}, "'\n"; + } + $glob = $g; + } + unless (defined $glob) { + die "Unknown ", + $_tag ? "tag" : "branch", + " destination $_branch_dest\n"; + } } } my ($lft, $rgt) = @{ $glob->{path} }{qw/left right/}; @@ -1107,6 +1131,14 @@ sub cmd_reset { print "r$r = $c ($gs->{ref_id})\n"; } +sub cmd_gc { + if (!$can_compress) { + warn "Compress::Zlib could not be found; unhandled.log " . + "files will not be compressed.\n"; + } + find({ wanted => \&gc_directory, no_chdir => 1}, "$ENV{GIT_DIR}/svn"); +} + ########################### utility functions ######################### sub rebase_cmd { @@ -1527,6 +1559,25 @@ sub md5sum { return $md5->hexdigest(); } +sub gc_directory { + if ($can_compress && -f $_ && basename($_) eq "unhandled.log") { + my $out_filename = $_ . ".gz"; + open my $in_fh, "<", $_ or die "Unable to open $_: $!\n"; + binmode $in_fh; + my $gz = Compress::Zlib::gzopen($out_filename, "ab") or + die "Unable to open $out_filename: $!\n"; + + my $res; + while ($res = sysread($in_fh, my $str, 1024)) { + $gz->gzwrite($str) or + die "Unable to write: ".$gz->gzerror()."!\n"; + } + unlink $_ or die "unlink $File::Find::name: $!\n"; + } elsif (-f $_ && basename($_) eq "index") { + unlink $_ or die "unlink $_: $!\n"; + } +} + package Git::SVN; use strict; use warnings; @@ -1647,6 +1698,7 @@ sub fetch_all { my $ra = Git::SVN::Ra->new($url); my $uuid = $ra->get_uuid; my $head = $ra->get_latest_revnum; + $ra->get_log("", $head, 0, 1, 0, 1, sub { $head = $_[1] }); my $base = defined $fetch ? $head : 0; # read the max revs for wildcard expansion (branches/*, tags/*) @@ -3953,7 +4005,7 @@ sub repo_path { sub url_path { my ($self, $path) = @_; if ($self->{url} =~ m#^https?://#) { - $path =~ s/([^~a-zA-Z0-9_.-])/uc sprintf("%%%02x",ord($1))/eg; + $path =~ s!([^~a-zA-Z0-9_./-])!uc sprintf("%%%02x",ord($1))!eg; } $self->{url} . '/' . $self->repo_path($path); } @@ -4779,7 +4831,11 @@ sub minimize_url { my $c = ''; do { $url .= "/$c" if length $c; - eval { (ref $self)->new($url)->get_latest_revnum }; + eval { + my $ra = (ref $self)->new($url); + my $latest = $ra->get_latest_revnum; + $ra->get_log("", $latest, 0, 1, 0, 1, sub {}); + }; } while ($@ && ($c = shift @components)); $url; } diff --git a/refs.c b/refs.c index e15880fbc8..e49eaa3089 100644 --- a/refs.c +++ b/refs.c @@ -531,9 +531,10 @@ static int do_one_ref(const char *base, each_ref_fn fn, int trim, { if (strncmp(base, entry->name, trim)) return 0; + /* Is this a "negative ref" that represents a deleted ref? */ + if (is_null_sha1(entry->sha1)) + return 0; if (!(flags & DO_FOR_EACH_INCLUDE_BROKEN)) { - if (is_null_sha1(entry->sha1)) - return 0; if (!has_sha1_file(entry->sha1)) { error("%s does not point to a valid object!", entry->name); return 0; diff --git a/t/t1300-repo-config.sh b/t/t1300-repo-config.sh index 43ea283242..8c43dcde8a 100755 --- a/t/t1300-repo-config.sh +++ b/t/t1300-repo-config.sh @@ -459,6 +459,28 @@ EOF test_expect_success "rename succeeded" "test_cmp expect .git/config" +cat >> .git/config << EOF +[branch "vier"] z = 1 +EOF + +test_expect_success "rename a section with a var on the same line" \ + 'git config --rename-section branch.vier branch.zwei' + +cat > expect << EOF +# Hallo + #Bello +[branch "zwei"] + x = 1 +[branch "zwei"] + y = 1 +[branch "drei"] +weird +[branch "zwei"] + z = 1 +EOF + +test_expect_success "rename succeeded" "test_cmp expect .git/config" + cat >> .git/config << EOF [branch "zwei"] a = 1 [branch "vier"] EOF diff --git a/t/t3414-rebase-preserve-onto.sh b/t/t3414-rebase-preserve-onto.sh new file mode 100755 index 0000000000..80019ee072 --- /dev/null +++ b/t/t3414-rebase-preserve-onto.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# +# Copyright (c) 2009 Greg Price +# + +test_description='git rebase -p should respect --onto + +In a rebase with --onto, we should rewrite all the commits that +aren'"'"'t on top of $ONTO, even if they are on top of $UPSTREAM. +' +. ./test-lib.sh + +. ../lib-rebase.sh + +# Set up branches like this: +# A1---B1---E1---F1---G1 +# \ \ / +# \ \--C1---D1--/ +# H1 + +test_expect_success 'setup' ' + test_commit A1 && + test_commit B1 && + test_commit C1 && + test_commit D1 && + git reset --hard B1 && + test_commit E1 && + test_commit F1 && + test_merge G1 D1 && + git reset --hard A1 && + test_commit H1 +' + +# Now rebase merge G1 from both branches' base B1, both should move: +# A1---B1---E1---F1---G1 +# \ \ / +# \ \--C1---D1--/ +# \ +# H1---E2---F2---G2 +# \ / +# \--C2---D2--/ + +test_expect_success 'rebase from B1 onto H1' ' + git checkout G1 && + git rebase -p --onto H1 B1 && + test "$(git rev-parse HEAD^1^1^1)" = "$(git rev-parse H1)" && + test "$(git rev-parse HEAD^2^1^1)" = "$(git rev-parse H1)" +' + +# On the other hand if rebase from E1 which is within one branch, +# then the other branch stays: +# A1---B1---E1---F1---G1 +# \ \ / +# \ \--C1---D1--/ +# \ \ +# H1-----F3-----G3 + +test_expect_success 'rebase from E1 onto H1' ' + git checkout G1 && + git rebase -p --onto H1 E1 && + test "$(git rev-parse HEAD^1^1)" = "$(git rev-parse H1)" && + test "$(git rev-parse HEAD^2)" = "$(git rev-parse D1)" +' + +# And the same if we rebase from a commit in the second-parent branch. +# A1---B1---E1---F1----G1 +# \ \ \ / +# \ \--C1---D1-\-/ +# \ \ +# H1------D3------G4 + +test_expect_success 'rebase from C1 onto H1' ' + git checkout G1 && + git rev-list --first-parent --pretty=oneline C1..G1 && + git rebase -p --onto H1 C1 && + test "$(git rev-parse HEAD^2^1)" = "$(git rev-parse H1)" && + test "$(git rev-parse HEAD^1)" = "$(git rev-parse F1)" +' + +test_done diff --git a/t/t4124-apply-ws-rule.sh b/t/t4124-apply-ws-rule.sh index 5698a9a736..fac2093d7f 100755 --- a/t/t4124-apply-ws-rule.sh +++ b/t/t4124-apply-ws-rule.sh @@ -154,7 +154,9 @@ create_patch () { index e69de29..8bd6648 100644 --- a/target +++ b/target - @@ -0,0 +1 @@ + @@ -0,0 +1,3 @@ + +An empty line follows + + +A line with trailing whitespace and no newline_ \ No newline at end of file EOF @@ -162,8 +164,10 @@ create_patch () { test_expect_success 'trailing whitespace & no newline at the end of file' ' >target && - create_patch | git apply --whitespace=fix - && - grep "newline$" target + create_patch >patch-file && + git apply --whitespace=fix patch-file && + grep "newline$" target && + grep "^$" target ' test_done diff --git a/t/t7700-repack.sh b/t/t7700-repack.sh index 87c9b0e121..f4aa054750 100755 --- a/t/t7700-repack.sh +++ b/t/t7700-repack.sh @@ -149,5 +149,17 @@ test_expect_success 'local packed unreachable obs that exist in alternate ODB ar test_must_fail git show $csha1 ' +test_expect_success 'objects made unreachable by grafts only are kept' ' + test_tick && + git commit --allow-empty -m "commit 4" && + H0=$(git rev-parse HEAD) && + H1=$(git rev-parse HEAD^) && + H2=$(git rev-parse HEAD^^) && + echo "$H0 $H2" > .git/info/grafts && + git reflog expire --expire=now --expire-unreachable=now --all && + git repack -a -d && + git cat-file -t $H1 + ' + test_done diff --git a/t/t8005-blame-i18n.sh b/t/t8005-blame-i18n.sh index 9cca14d080..cb390559f9 100755 --- a/t/t8005-blame-i18n.sh +++ b/t/t8005-blame-i18n.sh @@ -4,7 +4,7 @@ test_description='git blame encoding conversion' . ./test-lib.sh . "$TEST_DIRECTORY"/t8005/utf8.txt -. "$TEST_DIRECTORY"/t8005/iso8859-5.txt +. "$TEST_DIRECTORY"/t8005/euc-japan.txt . "$TEST_DIRECTORY"/t8005/sjis.txt test_expect_success 'setup the repository' ' @@ -13,10 +13,10 @@ test_expect_success 'setup the repository' ' git add file && git commit --author "$UTF8_NAME " -m "$UTF8_MSG" && - echo "ISO-8859-5 LINE" >> file && + echo "EUC-JAPAN LINE" >> file && git add file && - git config i18n.commitencoding ISO8859-5 && - git commit --author "$ISO8859_5_NAME " -m "$ISO8859_5_MSG" && + git config i18n.commitencoding eucJP && + git commit --author "$EUC_JAPAN_NAME " -m "$EUC_JAPAN_MSG" && echo "SJIS LINE" >> file && git add file && @@ -41,17 +41,17 @@ test_expect_success \ ' cat >expected < actual && test_cmp actual expected @@ -76,8 +76,8 @@ test_expect_success \ cat >expected < foo && + svn_cmd add foo && + svn_cmd commit -m "add foo" + ) +' + +start_httpd + +test_expect_success 'clone trunk with "-r HEAD"' ' + git svn clone -r HEAD "$svnrepo/trunk" g && + ( cd g && git rev-parse --symbolic --verify HEAD ) +' + +stop_httpd + +test_done diff --git a/t/t9143-git-svn-gc.sh b/t/t9143-git-svn-gc.sh new file mode 100755 index 0000000000..f2ba2d1da3 --- /dev/null +++ b/t/t9143-git-svn-gc.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# +# Copyright (c) 2009 Robert Allan Zeh + +test_description='git svn gc basic tests' + +. ./lib-git-svn.sh + +test_expect_success 'setup directories and test repo' ' + mkdir import && + mkdir tmp && + echo "Sample text for Subversion repository." > import/test.txt && + svn_cmd import -m "import for git svn" import "$svnrepo" > /dev/null + ' + +test_expect_success 'checkout working copy from svn' \ + 'svn_cmd co "$svnrepo" test_wc' + +test_expect_success 'set some properties to create an unhandled.log file' ' + ( + cd test_wc && + svn_cmd propset foo bar test.txt && + svn_cmd commit -m "property set" + )' + +test_expect_success 'Setup repo' 'git svn init "$svnrepo"' + +test_expect_success 'Fetch repo' 'git svn fetch' + +test_expect_success 'make backup copy of unhandled.log' ' + cp .git/svn/git-svn/unhandled.log tmp + ' + +test_expect_success 'create leftover index' '> .git/svn/git-svn/index' + +test_expect_success 'git svn gc runs' 'git svn gc' + +test_expect_success 'git svn index removed' '! test -f .git/svn/git-svn/index' + +if perl -MCompress::Zlib -e 0 2>/dev/null +then + test_expect_success 'git svn gc produces a valid gzip file' ' + gunzip .git/svn/git-svn/unhandled.log.gz + ' +else + say "Perl Compress::Zlib unavailable, skipping gunzip test" +fi + +test_expect_success 'git svn gc does not change unhandled.log files' ' + test_cmp .git/svn/git-svn/unhandled.log tmp/unhandled.log + ' + +test_done diff --git a/t/t9301-fast-export.sh b/t/t9301-fast-export.sh index 8c8a9e63c2..356964e53a 100755 --- a/t/t9301-fast-export.sh +++ b/t/t9301-fast-export.sh @@ -262,6 +262,94 @@ test_expect_success 'cope with tagger-less tags' ' ' +test_expect_success 'setup for limiting exports by PATH' ' + mkdir limit-by-paths && + cd limit-by-paths && + git init && + echo hi > there && + git add there && + git commit -m "First file" && + echo foo > bar && + git add bar && + git commit -m "Second file" && + git tag -a -m msg mytag && + echo morefoo >> bar && + git add bar && + git commit -m "Change to second file" && + cd .. +' + +cat > limit-by-paths/expected << EOF +blob +mark :1 +data 3 +hi + +reset refs/tags/mytag +commit refs/tags/mytag +mark :2 +author A U Thor 1112912713 -0700 +committer C O Mitter 1112912713 -0700 +data 11 +First file +M 100644 :1 there + +EOF + +test_expect_success 'dropping tag of filtered out object' ' + cd limit-by-paths && + git fast-export --tag-of-filtered-object=drop mytag -- there > output && + test_cmp output expected && + cd .. +' + +cat >> limit-by-paths/expected << EOF +tag mytag +from :2 +tagger C O Mitter 1112912713 -0700 +data 4 +msg + +EOF + +test_expect_success 'rewriting tag of filtered out object' ' + cd limit-by-paths && + git fast-export --tag-of-filtered-object=rewrite mytag -- there > output && + test_cmp output expected && + cd .. +' + +cat > limit-by-paths/expected << EOF +blob +mark :1 +data 4 +foo + +blob +mark :2 +data 3 +hi + +reset refs/heads/master +commit refs/heads/master +mark :3 +author A U Thor 1112912713 -0700 +committer C O Mitter 1112912713 -0700 +data 12 +Second file +M 100644 :1 bar +M 100644 :2 there + +EOF + +test_expect_failure 'no exact-ref revisions included' ' + cd limit-by-paths && + git fast-export master~2..master~1 > output && + test_cmp output expected && + cd .. +' + + test_expect_success 'set-up a few more tags for tag export tests' ' git checkout -f master && HEAD_TREE=`git show -s --pretty=raw HEAD | grep tree | sed "s/tree //"` && @@ -271,8 +359,14 @@ test_expect_success 'set-up a few more tags for tag export tests' ' git tag -a tag-obj_tag-obj -m "tagging a tag" tree_tag-obj ' +test_expect_success 'tree_tag' ' + mkdir result && + (cd result && git init) && + git fast-export tree_tag > fe-stream && + (cd result && git fast-import < ../fe-stream) +' + # NEEDSWORK: not just check return status, but validate the output -test_expect_success 'tree_tag' 'git fast-export tree_tag' test_expect_success 'tree_tag-obj' 'git fast-export tree_tag-obj' test_expect_success 'tag-obj_tag' 'git fast-export tag-obj_tag' test_expect_success 'tag-obj_tag-obj' 'git fast-export tag-obj_tag-obj' diff --git a/ws.c b/ws.c index 8d855b7fd5..59d0883c1f 100644 --- a/ws.c +++ b/ws.c @@ -262,10 +262,10 @@ int ws_fix_copy(char *dst, const char *src, int len, unsigned ws_rule, int *erro * Strip trailing whitespace */ if (ws_rule & WS_TRAILING_SPACE) { - if (1 < len && src[len - 1] == '\n') { + if (0 < len && src[len - 1] == '\n') { add_nl_to_tail = 1; len--; - if (1 < len && src[len - 1] == '\r') { + if (0 < len && src[len - 1] == '\r') { add_cr_to_tail = !!(ws_rule & WS_CR_AT_EOL); len--; }