diff options
70 files changed, 2556 insertions, 1411 deletions
diff --git a/.gitignore b/.gitignore index d706dd92c6..91e69665f4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ GIT-CFLAGS GIT-VERSION-FILE git git-add +git-add--interactive git-am git-annotate git-apply diff --git a/Documentation/Makefile b/Documentation/Makefile index d68bc4a788..93c7024b48 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -32,6 +32,7 @@ man7dir=$(mandir)/man7 # DESTDIR= INSTALL?=install +DOC_REF = origin/man -include ../config.mak.autogen @@ -112,3 +113,6 @@ $(patsubst %.txt,%.html,$(wildcard howto/*.txt)): %.html : %.txt install-webdoc : html sh ./install-webdoc.sh $(WEBDOC_DEST) + +quick-install: + sh ./install-doc-quick.sh $(DOC_REF) $(mandir) diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index d86c0e7f19..8710b3a75e 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -7,7 +7,7 @@ git-add - Add file contents to the changeset to be committed next SYNOPSIS -------- -'git-add' [-n] [-v] [--] <file>... +'git-add' [-n] [-v] [--interactive] [--] <file>... DESCRIPTION ----------- @@ -43,6 +43,10 @@ OPTIONS -v:: Be verbose. +\--interactive:: + Add modified contents in the working tree interactively to + the index. + \--:: This option can be used to separate command-line options from the list of files, (useful when filenames might be mistaken @@ -67,6 +71,119 @@ git-add git-*.sh:: (i.e. you are listing the files explicitly), it does not consider `subdir/git-foo.sh`. +Interactive mode +---------------- +When the command enters the interactive mode, it shows the +output of the 'status' subcommand, and then goes into ints +interactive command loop. + +The command loop shows the list of subcommands available, and +gives a prompt "What now> ". In general, when the prompt ends +with a single '>', you can pick only one of the choices given +and type return, like this: + +------------ + *** Commands *** + 1: status 2: update 3: revert 4: add untracked + 5: patch 6: diff 7: quit 8: help + What now> 1 +------------ + +You also could say "s" or "sta" or "status" above as long as the +choice is unique. + +The main command loop has 6 subcommands (plus help and quit). + +status:: + + This shows the change between HEAD and index (i.e. what will be + committed if you say "git commit"), and between index and + working tree files (i.e. what you could stage further before + "git commit" using "git-add") for each path. A sample output + looks like this: ++ +------------ + staged unstaged path + 1: binary nothing foo.png + 2: +403/-35 +1/-1 git-add--interactive.perl +------------ ++ +It shows that foo.png has differences from HEAD (but that is +binary so line count cannot be shown) and there is no +difference between indexed copy and the working tree +version (if the working tree version were also different, +'binary' would have been shown in place of 'nothing'). The +other file, git-add--interactive.perl, has 403 lines added +and 35 lines deleted if you commit what is in the index, but +working tree file has further modifications (one addition and +one deletion). + +update:: + + This shows the status information and gives prompt + "Update>>". When the prompt ends with double '>>', you can + make more than one selection, concatenated with whitespace or + comma. Also you can say ranges. E.g. "2-5 7,9" to choose + 2,3,4,5,7,9 from the list. You can say '*' to choose + everything. ++ +What you chose are then highlighted with '*', +like this: ++ +------------ + staged unstaged path + 1: binary nothing foo.png +* 2: +403/-35 +1/-1 git-add--interactive.perl +------------ ++ +To remove selection, prefix the input with `-` +like this: ++ +------------ +Update>> -2 +------------ ++ +After making the selection, answer with an empty line to stage the +contents of working tree files for selected paths in the index. + +revert:: + + This has a very similar UI to 'update', and the staged + information for selected paths are reverted to that of the + HEAD version. Reverting new paths makes them untracked. + +add untracked:: + + This has a very similar UI to 'update' and + 'revert', and lets you add untracked paths to the index. + +patch:: + + This lets you choose one path out of 'status' like selection. + After choosing the path, it presents diff between the index + and the working tree file and asks you if you want to stage + the change of each hunk. You can say: + + y - add the change from that hunk to index + n - do not add the change from that hunk to index + a - add the change from that hunk and all the rest to index + d - do not the change from that hunk nor any of the rest to index + j - do not decide on this hunk now, and view the next + undecided hunk + J - do not decide on this hunk now, and view the next hunk + k - do not decide on this hunk now, and view the previous + undecided hunk + K - do not decide on this hunk now, and view the previous hunk ++ +After deciding the fate for all hunks, if there is any hunk +that was chosen, the index is updated with the selected hunks. + +diff:: + + This lets you review what will be committed (i.e. between + HEAD and index). + + See Also -------- gitlink:git-status[1] diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 71417feba8..c464bd2fda 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -11,7 +11,7 @@ SYNOPSIS 'git-branch' [-r | -a] [-v [--abbrev=<length>]] 'git-branch' [-l] [-f] <branchname> [<start-point>] 'git-branch' (-m | -M) [<oldbranch>] <newbranch> -'git-branch' (-d | -D) <branchname>... +'git-branch' (-d | -D) [-r] <branchname>... DESCRIPTION ----------- @@ -33,7 +33,8 @@ to happen. With a `-d` or `-D` option, `<branchname>` will be deleted. You may specify more than one branch for deletion. If the branch currently -has a ref log then the ref log will also be deleted. +has a ref log then the ref log will also be deleted. Use -r together with -d +to delete remote-tracking branches. OPTIONS @@ -60,7 +61,7 @@ OPTIONS Move/rename a branch even if the new branchname already exists. -r:: - List the remote-tracking branches. + List or delete (if used with -d) the remote-tracking branches. -a:: List both remote-tracking branches and local branches. @@ -111,10 +112,12 @@ Delete unneeded branch:: ------------ $ git clone git://git.kernel.org/.../git.git my.git $ cd my.git -$ git branch -D todo <1> +$ git branch -d -r todo html man <1> +$ git branch -D test <2> ------------ + -<1> delete todo branch even if the "master" branch does not have all +<1> delete remote-tracking branches "todo", "html", "man" +<2> delete "test" branch even if the "master" branch does not have all commits from todo branch. diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index bfddb21fee..874934a332 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -11,8 +11,7 @@ SYNOPSIS [verse] 'git-clone' [--template=<template_directory>] [-l [-s]] [-q] [-n] [--bare] [-o <name>] [-u <upload-pack>] [--reference <repository>] - [--use-separate-remote | --no-separate-remote] <repository> - [<directory>] + <repository> [<directory>] DESCRIPTION ----------- @@ -99,18 +98,6 @@ OPTIONS if unset the templates are taken from the installation defined default, typically `/usr/share/git-core/templates`. ---use-separate-remote:: - Save remotes heads under `$GIT_DIR/refs/remotes/origin/` instead - of `$GIT_DIR/refs/heads/`. Only the local master branch is - saved in the latter. This is the default. - ---no-separate-remote:: - Save remotes heads in the same namespace as the local - heads, `$GIT_DIR/refs/heads/'. In regular repositories, - this is a legacy setup git-clone created by default in - older Git versions, and will be removed before the next - major release. - <repository>:: The (possibly remote) repository to clone from. It can be any URL git-fetch supports. diff --git a/Documentation/git-diff.txt b/Documentation/git-diff.txt index 10fdf88dce..8977877b21 100644 --- a/Documentation/git-diff.txt +++ b/Documentation/git-diff.txt @@ -8,7 +8,7 @@ git-diff - Show changes between commits, commit and working tree, etc SYNOPSIS -------- -'git-diff' [ --diff-options ] <tree-ish>{0,2} [--] [<path>...] +'git-diff' [ --diff-options ] <commit>{0,2} [--] [<path>...] DESCRIPTION ----------- @@ -26,7 +26,7 @@ tree and the index file, or the index file and the working tree. 'git-diff' [--options] --cached [<commit>] [--] [<path>...]:: This form is to view the changes you staged for the next - commit relative to the named <tree-ish>. Typically you + commit relative to the named <commit>. Typically you would want comparison with the latest commit, so if you do not give <commit>, it defaults to HEAD. diff --git a/Documentation/git-show-branch.txt b/Documentation/git-show-branch.txt index 948ff10e6c..dafacd4308 100644 --- a/Documentation/git-show-branch.txt +++ b/Documentation/git-show-branch.txt @@ -8,7 +8,7 @@ git-show-branch - Show branches and their commits SYNOPSIS -------- [verse] -'git-show-branch' [--all] [--heads] [--tags] [--topo-order] [--current] +'git-show-branch' [--all] [--remotes] [--topo-order] [--current] [--more=<n> | --list | --independent | --merge-base] [--no-name | --sha1-name] [--topics] [<rev> | <glob>]... @@ -37,9 +37,11 @@ OPTIONS branches under $GIT_DIR/refs/heads/topic, giving `topic/*` would show all of them. ---all --heads --tags:: - Show all refs under $GIT_DIR/refs, $GIT_DIR/refs/heads, - and $GIT_DIR/refs/tags, respectively. +-r|--remotes:: + Show the remote-tracking branches. + +-a|--all:: + Show both remote-tracking branches and local branches. --current:: With this option, the command includes the current diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index c589a98630..f5f57e8f87 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -64,7 +64,7 @@ manually joining branches on commit. against the latest changes in the SVN repository. An optional command-line argument may be specified as an alternative to HEAD. - This is advantageous over 'commit' (below) because it produces + This is advantageous over 'set-tree' (below) because it produces cleaner, more linear history. 'log':: @@ -89,7 +89,7 @@ manually joining branches on commit. Any other arguments are passed directly to `git log' -'commit':: +'set-tree':: You should consider using 'dcommit' instead of this command. Commit specified commit or tree objects to SVN. This relies on your imported fetch data being up-to-date. This makes @@ -172,7 +172,7 @@ This can allow you to make partial mirrors when running fetch. -:: --stdin:: -Only used with the 'commit' command. +Only used with the 'set-tree' command. Read a list of commits from stdin and commit them in reverse order. Only the leading sha1 is read from each line, so @@ -180,7 +180,7 @@ git-rev-list --pretty=oneline output can be used. --rmdir:: -Only used with the 'dcommit', 'commit' and 'commit-diff' commands. +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. Remove directories from the SVN tree if there are no files left behind. SVN can version empty directories, and they are not @@ -193,7 +193,7 @@ repo-config key: svn.rmdir -e:: --edit:: -Only used with the 'dcommit', 'commit' and 'commit-diff' commands. +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. Edit the commit message before committing to SVN. This is off by default for objects that are commits, and forced on when committing @@ -204,7 +204,7 @@ repo-config key: svn.edit -l<num>:: --find-copies-harder:: -Only used with the 'dcommit', 'commit' and 'commit-diff' commands. +Only used with the 'dcommit', 'set-tree' and 'commit-diff' commands. They are both passed directly to git-diff-tree see gitlink:git-diff-tree[1] for more information. @@ -276,7 +276,7 @@ ADVANCED OPTIONS -b<refname>:: --branch <refname>:: -Used with 'fetch', 'dcommit' or 'commit'. +Used with 'fetch', 'dcommit' or 'set-tree'. This can be used to join arbitrary git branches to remotes/git-svn on new commits where the tree object is equivalent. @@ -392,11 +392,11 @@ REBASE VS. PULL --------------- Originally, git-svn recommended that the remotes/git-svn branch be -pulled from. This is because the author favored 'git-svn commit B' -to commit a single head rather than the 'git-svn commit A..B' notation +pulled from. This is because the author favored 'git-svn set-tree B' +to commit a single head rather than the 'git-svn set-tree A..B' notation to commit multiple commits. -If you use 'git-svn commit A..B' to commit several diffs and you do not +If you use 'git-svn set-tree A..B' to commit several diffs and you do not have the latest remotes/git-svn merged into my-branch, you should use 'git rebase' to update your work branch instead of 'git pull'. 'pull' can cause non-linear history to be flattened when committing into SVN, diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt index 45476c2e41..48b82b86f8 100644 --- a/Documentation/git-tag.txt +++ b/Documentation/git-tag.txt @@ -9,7 +9,8 @@ git-tag - Create a tag object signed with GPG SYNOPSIS -------- [verse] -'git-tag' [-a | -s | -u <key-id>] [-f | -d] [-m <msg>] <name> [<head>] +'git-tag' [-a | -s | -u <key-id>] [-f | -d] [-m <msg> | -F <file>] + <name> [<head>] 'git-tag' -l [<pattern>] DESCRIPTION @@ -60,6 +61,9 @@ OPTIONS -m <msg>:: Use the given tag message (instead of prompting) +-F <file>:: + Take the tag message from the given file. Use '-' to + read the message from the standard input. Author ------ diff --git a/Documentation/install-doc-quick.sh b/Documentation/install-doc-quick.sh new file mode 100755 index 0000000000..a64054948a --- /dev/null +++ b/Documentation/install-doc-quick.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# This requires a branch named in $head +# (usually 'man' or 'html', provided by the git.git repository) +set -e +head="$1" +mandir="$2" +SUBDIRECTORY_OK=t +USAGE='<refname> <target directory>' +. git-sh-setup +export GIT_DIR + +test -z "$mandir" && usage +if ! git-rev-parse --verify "$head^0" >/dev/null; then + echo >&2 "head: $head does not exist in the current repository" + usage +fi + +GIT_INDEX_FILE=`pwd`/.quick-doc.index +export GIT_INDEX_FILE +rm -f "$GIT_INDEX_FILE" +git-read-tree $head +git-checkout-index -a -f --prefix="$mandir"/ + +if test -n "$GZ"; then + cd "$mandir" + for i in `git-ls-tree -r --name-only $head` + do + gzip < $i > $i.gz && rm $i + done +fi +rm -f "$GIT_INDEX_FILE" @@ -79,9 +79,6 @@ all: # # Define NO_ICONV if your libc does not properly support iconv. # -# Define NO_ACCURATE_DIFF if your diff program at least sometimes misses -# a missing newline at the end of the file. -# # Define USE_NSEC below if you want git to care about sub-second file mtimes # and ctimes. Note that you need recent glibc (at least 2.2.4) for this, and # it will BREAK YOUR LOCAL DIFFS! show-diff and anything using it will likely @@ -173,8 +170,8 @@ SCRIPT_SH = \ git-lost-found.sh git-quiltimport.sh SCRIPT_PERL = \ + git-add--interactive.perl \ git-archimport.perl git-cvsimport.perl git-relink.perl \ - git-rerere.perl \ git-cvsserver.perl \ git-svnimport.perl git-cvsexportcommit.perl \ git-send-email.perl git-svn.perl @@ -233,7 +230,8 @@ LIB_H = \ archive.h blob.h cache.h commit.h csum-file.h delta.h grep.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 + tree-walk.h log-tree.h dir.h path-list.h unpack-trees.h builtin.h \ + utf8.h DIFF_OBJS = \ diff.o diff-lib.o diffcore-break.o diffcore-order.o \ @@ -252,7 +250,8 @@ LIB_OBJS = \ revision.o pager.o tree-walk.o xdiff-interface.o \ write_or_die.o trace.o list-objects.o grep.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 + color.o wt-status.o archive-zip.o archive-tar.o \ + utf8.o BUILTIN_OBJS = \ builtin-add.o \ @@ -289,6 +288,7 @@ BUILTIN_OBJS = \ builtin-push.o \ builtin-read-tree.o \ builtin-repo-config.o \ + builtin-rerere.o \ builtin-rev-list.o \ builtin-rev-parse.o \ builtin-rm.o \ @@ -549,9 +549,6 @@ else endif endif endif -ifdef NO_ACCURATE_DIFF - BASIC_CFLAGS += -DNO_ACCURATE_DIFF -endif ifdef NO_PERL_MAKEMAKER export NO_PERL_MAKEMAKER endif @@ -830,6 +827,8 @@ install: all install-doc: $(MAKE) -C Documentation install +quick-install-doc: + $(MAKE) -C Documentation quick-install diff --git a/builtin-add.c b/builtin-add.c index c8a114fefb..17641b433d 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -6,10 +6,11 @@ #include "cache.h" #include "builtin.h" #include "dir.h" +#include "exec_cmd.h" #include "cache-tree.h" static const char builtin_add_usage[] = -"git-add [-n] [-v] <filepattern>..."; +"git-add [-n] [-v] [--interactive] [--] <filepattern>..."; static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix) { @@ -87,6 +88,20 @@ int cmd_add(int argc, const char **argv, const char *prefix) int verbose = 0, show_only = 0; const char **pathspec; struct dir_struct dir; + int add_interactive = 0; + + for (i = 1; i < argc; i++) { + if (!strcmp("--interactive", argv[i])) + add_interactive++; + } + if (add_interactive) { + const char *args[] = { "add--interactive", NULL }; + + if (add_interactive != 1 || argc != 2) + die("add --interactive does not take any parameters"); + execv_git_cmd(args); + exit(1); + } git_config(git_default_config); @@ -111,6 +126,11 @@ int cmd_add(int argc, const char **argv, const char *prefix) } usage(builtin_add_usage); } + if (argc <= i) { + fprintf(stderr, "Nothing specified, nothing added.\n"); + fprintf(stderr, "Maybe you wanted to say 'git add .'?\n"); + return 0; + } pathspec = get_pathspec(prefix, argv + i); fill_directory(&dir, pathspec); diff --git a/builtin-blame.c b/builtin-blame.c index 9bf6ec951f..4a1accf13c 100644 --- a/builtin-blame.c +++ b/builtin-blame.c @@ -18,7 +18,9 @@ static char blame_usage[] = "git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [commit] [--] file\n" " -c, --compatibility Use the same output mode as git-annotate (Default: off)\n" +" -b Show blank SHA-1 for boundary commits (Default: off)\n" " -l, --long Show long commit SHA1 (Default: off)\n" +" --root Do not treat root commits as boundaries (Default: off)\n" " -t, --time Show raw timestamp (Default: off)\n" " -f, --show-name Show original filename (Default: auto)\n" " -n, --show-number Show original linenumber (Default: off)\n" @@ -32,6 +34,8 @@ static int longest_author; static int max_orig_digits; static int max_digits; static int max_score_digits; +static int show_root; +static int blank_boundary; #ifndef DEBUG #define DEBUG 0 @@ -1091,6 +1095,9 @@ static void assign_blame(struct scoreboard *sb, struct rev_info *revs, int opt) if (commit->object.parsed) mark_parents_uninteresting(commit); } + /* treat root commit as boundary */ + if (!commit->parents && !show_root) + commit->object.flags |= UNINTERESTING; /* Take responsibility for the remaining entries */ for (ent = sb->ent; ent; ent = ent->next) @@ -1314,8 +1321,12 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt) int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? 40 : 8; if (suspect->commit->object.flags & UNINTERESTING) { - length--; - putchar('^'); + if (!blank_boundary) { + length--; + putchar('^'); + } + else + memset(hex, ' ', length); } printf("%.*s", length, hex); @@ -1635,6 +1646,19 @@ static void prepare_blame_range(struct scoreboard *sb, usage(blame_usage); } +static int git_blame_config(const char *var, const char *value) +{ + if (!strcmp(var, "blame.showroot")) { + show_root = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "blame.blankboundary")) { + blank_boundary = git_config_bool(var, value); + return 0; + } + return git_default_config(var, value); +} + int cmd_blame(int argc, const char **argv, const char *prefix) { struct rev_info revs; @@ -1650,6 +1674,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix) char type[10]; const char *bottomtop = NULL; + git_config(git_blame_config); save_commit_buffer = 0; opt = 0; @@ -1658,6 +1683,10 @@ int cmd_blame(int argc, const char **argv, const char *prefix) const char *arg = argv[i]; if (*arg != '-') break; + else if (!strcmp("-b", arg)) + blank_boundary = 1; + else if (!strcmp("--root", arg)) + show_root = 1; else if (!strcmp("-c", arg)) output_option |= OUTPUT_ANNOTATE_COMPAT; else if (!strcmp("-t", arg)) diff --git a/builtin-branch.c b/builtin-branch.c index 515330c155..903d5cf056 100644 --- a/builtin-branch.c +++ b/builtin-branch.c @@ -12,8 +12,12 @@ #include "builtin.h" static const char builtin_branch_usage[] = - "git-branch (-d | -D) <branchname> | [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [-r | -a] [-v [--abbrev=<length>]]"; + "git-branch [-r] (-d | -D) <branchname> | [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [-r | -a] [-v [--abbrev=<length>]]"; +#define REF_UNKNOWN_TYPE 0x00 +#define REF_LOCAL_BRANCH 0x01 +#define REF_REMOTE_BRANCH 0x02 +#define REF_TAG 0x04 static const char *head; static unsigned char head_sha1[20]; @@ -89,12 +93,28 @@ static int in_merge_bases(const unsigned char *sha1, return ret; } -static void delete_branches(int argc, const char **argv, int force) +static int delete_branches(int argc, const char **argv, int force, int kinds) { struct commit *rev, *head_rev = head_rev; unsigned char sha1[20]; - char *name; + char *name = NULL; + const char *fmt, *remote; int i; + int ret = 0; + + switch (kinds) { + case REF_REMOTE_BRANCH: + fmt = "refs/remotes/%s"; + remote = "remote "; + force = 1; + break; + case REF_LOCAL_BRANCH: + fmt = "refs/heads/%s"; + remote = ""; + break; + default: + die("cannot use -a with -d"); + } if (!force) { head_rev = lookup_commit_reference(head_sha1); @@ -102,16 +122,30 @@ static void delete_branches(int argc, const char **argv, int force) die("Couldn't look up commit object for HEAD"); } for (i = 0; i < argc; i++) { - if (!strcmp(head, argv[i])) - die("Cannot delete the branch you are currently on."); + if (kinds == REF_LOCAL_BRANCH && !strcmp(head, argv[i])) { + error("Cannot delete the branch '%s' " + "which you are currently on.", argv[i]); + ret = 1; + continue; + } + + if (name) + free(name); - name = xstrdup(mkpath("refs/heads/%s", argv[i])); - if (!resolve_ref(name, sha1, 1, NULL)) - die("Branch '%s' not found.", argv[i]); + name = xstrdup(mkpath(fmt, argv[i])); + if (!resolve_ref(name, sha1, 1, NULL)) { + error("%sbranch '%s' not found.", + remote, argv[i]); + ret = 1; + continue; + } rev = lookup_commit_reference(sha1); - if (!rev) - die("Couldn't look up commit object for '%s'", name); + if (!rev) { + error("Couldn't look up commit object for '%s'", name); + ret = 1; + continue; + } /* This checks whether the merge bases of branch and * HEAD contains branch -- which means that the HEAD @@ -120,26 +154,28 @@ static void delete_branches(int argc, const char **argv, int force) if (!force && !in_merge_bases(sha1, rev, head_rev)) { - fprintf(stderr, - "The branch '%s' is not a strict subset of your current HEAD.\n" - "If you are sure you want to delete it, run 'git branch -D %s'.\n", - argv[i], argv[i]); - exit(1); + error("The branch '%s' is not a strict subset of " + "your current HEAD.\n" + "If you are sure you want to delete it, " + "run 'git branch -D %s'.", argv[i], argv[i]); + ret = 1; + continue; } - if (delete_ref(name, sha1)) - printf("Error deleting branch '%s'\n", argv[i]); - else - printf("Deleted branch %s.\n", argv[i]); + if (delete_ref(name, sha1)) { + error("Error deleting %sbranch '%s'", remote, + argv[i]); + ret = 1; + } else + printf("Deleted %sbranch %s.\n", remote, argv[i]); - free(name); } -} -#define REF_UNKNOWN_TYPE 0x00 -#define REF_LOCAL_BRANCH 0x01 -#define REF_REMOTE_BRANCH 0x02 -#define REF_TAG 0x04 + if (name) + free(name); + + return(ret); +} struct ref_item { char *name; @@ -435,7 +471,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) head += 11; if (delete) - delete_branches(argc - i, argv + i, force_delete); + return delete_branches(argc - i, argv + i, force_delete, kinds); else if (i == argc) print_ref_list(kinds, verbose, abbrev); else if (rename && (i == argc - 1)) diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c index 856f3cd841..f641787988 100644 --- a/builtin-commit-tree.c +++ b/builtin-commit-tree.c @@ -7,6 +7,7 @@ #include "commit.h" #include "tree.h" #include "builtin.h" +#include "utf8.h" #define BLOCKING (1ul << 14) @@ -32,7 +33,7 @@ static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) len = vsnprintf(one_line, sizeof(one_line), fmt, args); va_end(args); size = *sizep; - newsize = size + len; + newsize = size + len + 1; alloc = (size + 32767) & ~32767; buf = *bufp; if (newsize > alloc) { @@ -40,7 +41,7 @@ static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) buf = xrealloc(buf, alloc); *bufp = buf; } - *sizep = newsize; + *sizep = newsize - 1; memcpy(buf + size, one_line, len); } @@ -77,6 +78,11 @@ static int new_parent(int idx) return 1; } +static const char commit_utf8_warn[] = +"Warning: commit message does not conform to UTF-8.\n" +"You may want to amend it after fixing the message, or set the config\n" +"variable i18n.commitencoding to the encoding your project uses.\n"; + int cmd_commit_tree(int argc, const char **argv, const char *prefix) { int i; @@ -101,6 +107,9 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix) a = argv[i]; b = argv[i+1]; if (!b || strcmp(a, "-p")) usage(commit_tree_usage); + + if (parents >= MAXPARENT) + die("Too many parents (%d max)", MAXPARENT); if (get_sha1(b, parent_sha1[parents])) die("Not a valid object name %s", b); check_valid(parent_sha1[parents], commit_type); @@ -127,6 +136,11 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix) while (fgets(comment, sizeof(comment), stdin) != NULL) add_buffer(&buffer, &size, "%s", comment); + /* And check the encoding */ + buffer[size] = '\0'; + if (!strcmp(git_commit_encoding, "utf-8") && !is_utf8(buffer)) + fprintf(stderr, commit_utf8_warn); + if (!write_sha1_file(buffer, size, commit_type, commit_sha1)) { printf("%s\n", sha1_to_hex(commit_sha1)); return 0; diff --git a/builtin-init-db.c b/builtin-init-db.c index 1d7d15e8d5..01f366ad0b 100644 --- a/builtin-init-db.c +++ b/builtin-init-db.c @@ -124,8 +124,11 @@ static void copy_templates(const char *git_dir, int len, const char *template_di int template_len; DIR *dir; - if (!template_dir) - template_dir = DEFAULT_GIT_TEMPLATE_DIR; + if (!template_dir) { + template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); + if (!template_dir) + template_dir = DEFAULT_GIT_TEMPLATE_DIR; + } strcpy(template_path, template_dir); template_len = strlen(template_path); if (template_path[template_len-1] != '/') { diff --git a/builtin-merge-file.c b/builtin-merge-file.c index 6c4c3a3513..9135773908 100644 --- a/builtin-merge-file.c +++ b/builtin-merge-file.c @@ -1,26 +1,10 @@ #include "cache.h" #include "xdiff/xdiff.h" +#include "xdiff-interface.h" static const char merge_file_usage[] = "git merge-file [-p | --stdout] [-q | --quiet] [-L name1 [-L orig [-L name2]]] file1 orig_file file2"; -static int read_file(mmfile_t *ptr, const char *filename) -{ - struct stat st; - FILE *f; - - if (stat(filename, &st)) - return error("Could not stat %s", filename); - if ((f = fopen(filename, "rb")) == NULL) - return error("Could not open %s", filename); - ptr->ptr = xmalloc(st.st_size); - if (fread(ptr->ptr, st.st_size, 1, f) != 1) - return error("Could not read %s", filename); - fclose(f); - ptr->size = st.st_size; - return 0; -} - int cmd_merge_file(int argc, char **argv, char **envp) { char *names[3]; @@ -53,7 +37,7 @@ int cmd_merge_file(int argc, char **argv, char **envp) names[i] = argv[i + 1]; for (i = 0; i < 3; i++) - if (read_file(mmfs + i, argv[i + 1])) + if (read_mmfile(mmfs + i, argv[i + 1])) return -1; ret = xdl_merge(mmfs + 1, mmfs + 0, names[0], mmfs + 2, names[2], diff --git a/builtin-repo-config.c b/builtin-repo-config.c index a7ab4cee58..90633119d4 100644 --- a/builtin-repo-config.c +++ b/builtin-repo-config.c @@ -66,10 +66,10 @@ static int get_value(const char* key_, const char* regex_) char *global = NULL, *repo_config = NULL; const char *local; - local = getenv("GIT_CONFIG"); + local = getenv(CONFIG_ENVIRONMENT); if (!local) { const char *home = getenv("HOME"); - local = getenv("GIT_CONFIG_LOCAL"); + local = getenv(CONFIG_LOCAL_ENVIRONMENT); if (!local) local = repo_config = xstrdup(git_path("config")); if (home) diff --git a/builtin-rerere.c b/builtin-rerere.c new file mode 100644 index 0000000000..d064bd8bf0 --- /dev/null +++ b/builtin-rerere.c @@ -0,0 +1,406 @@ +#include "cache.h" +#include "path-list.h" +#include "xdiff/xdiff.h" +#include "xdiff-interface.h" + +#include <time.h> + +static const char git_rerere_usage[] = +"git-rerere [clear | status | diff | gc]"; + +/* these values are days */ +static int cutoff_noresolve = 15; +static int cutoff_resolve = 60; + +static char *merge_rr_path; + +static const char *rr_path(const char *name, const char *file) +{ + return git_path("rr-cache/%s/%s", name, file); +} + +static void read_rr(struct path_list *rr) +{ + unsigned char sha1[20]; + char buf[PATH_MAX]; + FILE *in = fopen(merge_rr_path, "r"); + if (!in) + return; + while (fread(buf, 40, 1, in) == 1) { + int i; + char *name; + if (get_sha1_hex(buf, sha1)) + die("corrupt MERGE_RR"); + buf[40] = '\0'; + name = xstrdup(buf); + if (fgetc(in) != '\t') + die("corrupt MERGE_RR"); + for (i = 0; i < sizeof(buf) && (buf[i] = fgetc(in)); i++) + ; /* do nothing */ + if (i == sizeof(buf)) + die("filename too long"); + path_list_insert(buf, rr)->util = xstrdup(name); + } + fclose(in); +} + +static struct lock_file write_lock; + +static int write_rr(struct path_list *rr, int out_fd) +{ + int i; + for (i = 0; i < rr->nr; i++) { + const char *path = rr->items[i].path; + write(out_fd, rr->items[i].util, 40); + write(out_fd, "\t", 1); + write(out_fd, path, strlen(path) + 1); + } + close(out_fd); + return commit_lock_file(&write_lock); +} + +struct buffer { + char *ptr; + int nr, alloc; +}; + +static void append_line(struct buffer *buffer, const char *line) +{ + int len = strlen(line); + + if (buffer->nr + len > buffer->alloc) { + buffer->alloc = alloc_nr(buffer->nr + len); + buffer->ptr = xrealloc(buffer->ptr, buffer->alloc); + } + memcpy(buffer->ptr + buffer->nr, line, len); + buffer->nr += len; +} + +static int handle_file(const char *path, + unsigned char *sha1, const char *output) +{ + SHA_CTX ctx; + char buf[1024]; + int hunk = 0, hunk_no = 0; + struct buffer minus = { NULL, 0, 0 }, plus = { NULL, 0, 0 }; + struct buffer *one = &minus, *two = + + FILE *f = fopen(path, "r"); + FILE *out; + + if (!f) + return error("Could not open %s", path); + + if (output) { + out = fopen(output, "w"); + if (!out) { + fclose(f); + return error("Could not write %s", output); + } + } else + out = NULL; + + if (sha1) + SHA1_Init(&ctx); + + while (fgets(buf, sizeof(buf), f)) { + if (!strncmp("<<<<<<< ", buf, 8)) + hunk = 1; + else if (!strncmp("=======", buf, 7)) + hunk = 2; + else if (!strncmp(">>>>>>> ", buf, 8)) { + hunk_no++; + hunk = 0; + if (memcmp(one->ptr, two->ptr, one->nr < two->nr ? + one->nr : two->nr) > 0) { + struct buffer *swap = one; + one = two; + two = swap; + } + if (out) { + fputs("<<<<<<<\n", out); + fwrite(one->ptr, one->nr, 1, out); + fputs("=======\n", out); + fwrite(two->ptr, two->nr, 1, out); + fputs(">>>>>>>\n", out); + } + if (sha1) { + SHA1_Update(&ctx, one->ptr, one->nr); + SHA1_Update(&ctx, "\0", 1); + SHA1_Update(&ctx, two->ptr, two->nr); + SHA1_Update(&ctx, "\0", 1); + } + } else if (hunk == 1) + append_line(one, buf); + else if (hunk == 2) + append_line(two, buf); + else if (out) + fputs(buf, out); + } + + fclose(f); + if (out) + fclose(out); + if (sha1) + SHA1_Final(sha1, &ctx); + return hunk_no; +} + +static int find_conflict(struct path_list *conflict) +{ + int i; + if (read_cache() < 0) + return error("Could not read index"); + for (i = 0; i + 2 < active_nr; i++) { + struct cache_entry *e1 = active_cache[i]; + struct cache_entry *e2 = active_cache[i + 1]; + struct cache_entry *e3 = active_cache[i + 2]; + if (ce_stage(e1) == 1 && ce_stage(e2) == 2 && + ce_stage(e3) == 3 && ce_same_name(e1, e2) && + ce_same_name(e1, e3)) { + path_list_insert((const char *)e1->name, conflict); + i += 3; + } + } + return 0; +} + +static int merge(const char *name, const char *path) +{ + int ret; + mmfile_t cur, base, other; + mmbuffer_t result = {NULL, 0}; + xpparam_t xpp = {XDF_NEED_MINIMAL}; + + if (handle_file(path, NULL, rr_path(name, "thisimage")) < 0) + return 1; + + if (read_mmfile(&cur, rr_path(name, "thisimage")) || + read_mmfile(&base, rr_path(name, "preimage")) || + read_mmfile(&other, rr_path(name, "postimage"))) + return 1; + ret = xdl_merge(&base, &cur, "", &other, "", + &xpp, XDL_MERGE_ZEALOUS, &result); + if (!ret) { + FILE *f = fopen(path, "w"); + if (!f) + return error("Could not write to %s", path); + fwrite(result.ptr, result.size, 1, f); + fclose(f); + } + + free(cur.ptr); + free(base.ptr); + free(other.ptr); + free(result.ptr); + + return ret; +} + +static void unlink_rr_item(const char *name) +{ + unlink(rr_path(name, "thisimage")); + unlink(rr_path(name, "preimage")); + unlink(rr_path(name, "postimage")); + rmdir(git_path("rr-cache/%s", name)); +} + +static void garbage_collect(struct path_list *rr) +{ + struct path_list to_remove = { NULL, 0, 0, 1 }; + char buf[1024]; + DIR *dir; + struct dirent *e; + int len, i, cutoff; + time_t now = time(NULL), then; + + strlcpy(buf, git_path("rr-cache"), sizeof(buf)); + len = strlen(buf); + dir = opendir(buf); + strcpy(buf + len++, "/"); + while ((e = readdir(dir))) { + const char *name = e->d_name; + struct stat st; + if (name[0] == '.' && (name[1] == '\0' || + (name[1] == '.' && name[2] == '\0'))) + continue; + i = snprintf(buf + len, sizeof(buf) - len, "%s", name); + strlcpy(buf + len + i, "/preimage", sizeof(buf) - len - i); + if (stat(buf, &st)) + continue; + then = st.st_mtime; + strlcpy(buf + len + i, "/postimage", sizeof(buf) - len - i); + cutoff = stat(buf, &st) ? cutoff_noresolve : cutoff_resolve; + if (then < now - cutoff * 86400) { + buf[len + i] = '\0'; + path_list_insert(xstrdup(name), &to_remove); + } + } + for (i = 0; i < to_remove.nr; i++) + unlink_rr_item(to_remove.items[i].path); + path_list_clear(&to_remove, 0); +} + +static int outf(void *dummy, mmbuffer_t *ptr, int nbuf) +{ + int i; + for (i = 0; i < nbuf; i++) + write(1, ptr[i].ptr, ptr[i].size); + return 0; +} + +static int diff_two(const char *file1, const char *label1, + const char *file2, const char *label2) +{ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb; + mmfile_t minus, plus; + + if (read_mmfile(&minus, file1) || read_mmfile(&plus, file2)) + return 1; + + printf("--- a/%s\n+++ b/%s\n", label1, label2); + fflush(stdout); + xpp.flags = XDF_NEED_MINIMAL; + xecfg.ctxlen = 3; + xecfg.flags = 0; + ecb.outf = outf; + xdl_diff(&minus, &plus, &xpp, &xecfg, &ecb); + + free(minus.ptr); + free(plus.ptr); + return 0; +} + +static int copy_file(const char *src, const char *dest) +{ + FILE *in, *out; + char buffer[32768]; + int count; + + if (!(in = fopen(src, "r"))) + return error("Could not open %s", src); + if (!(out = fopen(dest, "w"))) + return error("Could not open %s", dest); + while ((count = fread(buffer, 1, sizeof(buffer), in))) + fwrite(buffer, 1, count, out); + fclose(in); + fclose(out); + return 0; +} + +static int do_plain_rerere(struct path_list *rr, int fd) +{ + struct path_list conflict = { NULL, 0, 0, 1 }; + int i; + + find_conflict(&conflict); + + /* + * MERGE_RR records paths with conflicts immediately after merge + * failed. Some of the conflicted paths might have been hand resolved + * in the working tree since then, but the initial run would catch all + * and register their preimages. + */ + + for (i = 0; i < conflict.nr; i++) { + const char *path = conflict.items[i].path; + if (!path_list_has_path(rr, path)) { + unsigned char sha1[20]; + char *hex; + int ret; + ret = handle_file(path, sha1, NULL); + if (ret < 1) + continue; + hex = xstrdup(sha1_to_hex(sha1)); + path_list_insert(path, rr)->util = hex; + if (mkdir(git_path("rr-cache/%s", hex), 0755)) + continue;; + handle_file(path, NULL, rr_path(hex, "preimage")); + fprintf(stderr, "Recorded preimage for '%s'\n", path); + } + } + + /* + * Now some of the paths that had conflicts earlier might have been + * hand resolved. Others may be similar to a conflict already that + * was resolved before. + */ + + for (i = 0; i < rr->nr; i++) { + struct stat st; + int ret; + const char *path = rr->items[i].path; + const char *name = (const char *)rr->items[i].util; + + if (!stat(rr_path(name, "preimage"), &st) && + !stat(rr_path(name, "postimage"), &st)) { + if (!merge(name, path)) { + fprintf(stderr, "Resolved '%s' using " + "previous resolution.\n", path); + goto tail_optimization; + } + } + + /* Let's see if we have resolved it. */ + ret = handle_file(path, NULL, NULL); + if (ret) + continue; + + fprintf(stderr, "Recorded resolution for '%s'.\n", path); + copy_file(path, rr_path(name, "postimage")); +tail_optimization: + if (i < rr->nr - 1) { + memmove(rr->items + i, + rr->items + i + 1, + rr->nr - i - 1); + } + rr->nr--; + i--; + } + + return write_rr(rr, fd); +} + +int cmd_rerere(int argc, const char **argv, const char *prefix) +{ + struct path_list merge_rr = { NULL, 0, 0, 1 }; + int i, fd = -1; + struct stat st; + + if (stat(git_path("rr-cache"), &st) || !S_ISDIR(st.st_mode)) + return 0; + + merge_rr_path = xstrdup(git_path("rr-cache/MERGE_RR")); + fd = hold_lock_file_for_update(&write_lock, merge_rr_path, 1); + read_rr(&merge_rr); + + if (argc < 2) + return do_plain_rerere(&merge_rr, fd); + else if (!strcmp(argv[1], "clear")) { + for (i = 0; i < merge_rr.nr; i++) { + const char *name = (const char *)merge_rr.items[i].util; + if (!stat(git_path("rr-cache/%s", name), &st) && + S_ISDIR(st.st_mode) && + stat(rr_path(name, "postimage"), &st)) + unlink_rr_item(name); + } + unlink(merge_rr_path); + } else if (!strcmp(argv[1], "gc")) + garbage_collect(&merge_rr); + else if (!strcmp(argv[1], "status")) + for (i = 0; i < merge_rr.nr; i++) + printf("%s\n", merge_rr.items[i].path); + else if (!strcmp(argv[1], "diff")) + for (i = 0; i < merge_rr.nr; i++) { + const char *path = merge_rr.items[i].path; + const char *name = (const char *)merge_rr.items[i].util; + diff_two(rr_path(name, "preimage"), path, path, path); + } + else + usage(git_rerere_usage); + + path_list_clear(&merge_rr, 1); + return 0; +} + diff --git a/builtin-rev-list.c b/builtin-rev-list.c index fb7fc92145..1bb3a06680 100644 --- a/builtin-rev-list.c +++ b/builtin-rev-list.c @@ -54,6 +54,12 @@ static void show_commit(struct commit *commit) fputs(header_prefix, stdout); if (commit->object.flags & BOUNDARY) putchar('-'); + else if (revs.left_right) { + if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } if (revs.abbrev_commit && revs.abbrev) fputs(find_unique_abbrev(commit->object.sha1, revs.abbrev), stdout); diff --git a/builtin-show-branch.c b/builtin-show-branch.c index b9d9781d4d..c67f2fa2fe 100644 --- a/builtin-show-branch.c +++ b/builtin-show-branch.c @@ -4,7 +4,7 @@ #include "builtin.h" static const char show_branch_usage[] = -"git-show-branch [--sparse] [--current] [--all] [--heads] [--tags] [--topo-order] [--more=count | --list | --independent | --merge-base ] [--topics] [<refs>...] | --reflog[=n] <branch>"; +"git-show-branch [--sparse] [--current] [--all] [--remotes] [--topo-order] [--more=count | --list | --independent | --merge-base ] [--topics] [<refs>...] | --reflog[=n] <branch>"; static int default_num; static int default_alloc; @@ -383,6 +383,20 @@ static int append_head_ref(const char *refname, const unsigned char *sha1, int f return append_ref(refname + ofs, sha1, flag, cb_data); } +static int append_remote_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) +{ + unsigned char tmp[20]; + int ofs = 13; + if (strncmp(refname, "refs/remotes/", ofs)) + return 0; + /* If both heads/foo and tags/foo exists, get_sha1 would + * get confused. + */ + if (get_sha1(refname + ofs, tmp) || hashcmp(tmp, sha1)) + ofs = 5; + return append_ref(refname + ofs, sha1, flag, cb_data); +} + static int append_tag_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data) { if (strncmp(refname, "refs/tags/", 10)) @@ -423,16 +437,16 @@ static int append_matching_ref(const char *refname, const unsigned char *sha1, i return append_ref(refname, sha1, flag, cb_data); } -static void snarf_refs(int head, int tag) +static void snarf_refs(int head, int remotes) { if (head) { int orig_cnt = ref_name_cnt; for_each_ref(append_head_ref, NULL); sort_ref_range(orig_cnt, ref_name_cnt); } - if (tag) { + if (remotes) { int orig_cnt = ref_name_cnt; - for_each_ref(append_tag_ref, NULL); + for_each_ref(append_remote_ref, NULL); sort_ref_range(orig_cnt, ref_name_cnt); } } @@ -554,7 +568,7 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) struct commit_list *list = NULL, *seen = NULL; unsigned int rev_mask[MAX_REVS]; int num_rev, i, extra = 0; - int all_heads = 0, all_tags = 0; + int all_heads = 0, all_remotes = 0; int all_mask, all_revs; int lifo = 1; char head[128]; @@ -586,12 +600,10 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) ac--; av++; break; } - else if (!strcmp(arg, "--all")) - all_heads = all_tags = 1; - else if (!strcmp(arg, "--heads")) - all_heads = 1; - else if (!strcmp(arg, "--tags")) - all_tags = 1; + else if (!strcmp(arg, "--all") || !strcmp(arg, "-a")) + all_heads = all_remotes = 1; + else if (!strcmp(arg, "--remotes") || !strcmp(arg, "-r")) + all_remotes = 1; else if (!strcmp(arg, "--more")) extra = 1; else if (!strcmp(arg, "--list")) @@ -636,11 +648,11 @@ int cmd_show_branch(int ac, const char **av, const char *prefix) usage(show_branch_usage); /* If nothing is specified, show all branches by default */ - if (ac + all_heads + all_tags == 0) + if (ac + all_heads + all_remotes == 0) all_heads = 1; - if (all_heads + all_tags) - snarf_refs(all_heads, all_tags); + if (all_heads + all_remotes) + snarf_refs(all_heads, all_remotes); if (reflog) { int reflen; if (!ac) @@ -52,6 +52,7 @@ extern int cmd_prune_packed(int argc, const char **argv, const char *prefix); extern int cmd_push(int argc, const char **argv, const char *prefix); extern int cmd_read_tree(int argc, const char **argv, const char *prefix); extern int cmd_repo_config(int argc, const char **argv, const char *prefix); +extern int cmd_rerere(int argc, const char **argv, const char *prefix); extern int cmd_rev_list(int argc, const char **argv, const char *prefix); extern int cmd_rev_parse(int argc, const char **argv, const char *prefix); extern int cmd_rm(int argc, const char **argv, const char *prefix); @@ -122,6 +122,10 @@ extern int cache_errno; #define DB_ENVIRONMENT "GIT_OBJECT_DIRECTORY" #define INDEX_ENVIRONMENT "GIT_INDEX_FILE" #define GRAFT_ENVIRONMENT "GIT_GRAFT_FILE" +#define TEMPLATE_DIR_ENVIRONMENT "GIT_TEMPLATE_DIR" +#define CONFIG_ENVIRONMENT "GIT_CONFIG" +#define CONFIG_LOCAL_ENVIRONMENT "GIT_CONFIG_LOCAL" +#define EXEC_PATH_ENVIRONMENT "GIT_EXEC_PATH" extern int is_bare_git_dir(const char *dir); extern const char *get_git_dir(void); @@ -868,11 +868,11 @@ void sort_in_topological_order_fn(struct commit_list ** list, int lifo, /* merge-rebase stuff */ -/* bits #0..7 in revision.h */ -#define PARENT1 (1u<< 8) -#define PARENT2 (1u<< 9) -#define STALE (1u<<10) -#define RESULT (1u<<11) +/* bits #0..15 in revision.h */ +#define PARENT1 (1u<<16) +#define PARENT2 (1u<<17) +#define STALE (1u<<18) +#define RESULT (1u<<19) static struct commit *interesting(struct commit_list *list) { diff --git a/compat/inet_ntop.c b/compat/inet_ntop.c index ec8c1bff53..4d7ab9d975 100644 --- a/compat/inet_ntop.c +++ b/compat/inet_ntop.c @@ -93,7 +93,7 @@ inet_ntop6(src, dst, size) */ char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"], *tp; struct { int base, len; } best, cur; - u_int words[NS_IN6ADDRSZ / NS_INT16SZ]; + unsigned int words[NS_IN6ADDRSZ / NS_INT16SZ]; int i; /* diff --git a/compat/mmap.c b/compat/mmap.c index 0fd46e793d..4cfaee3136 100644 --- a/compat/mmap.c +++ b/compat/mmap.c @@ -1,17 +1,11 @@ #include "../git-compat-util.h" -void *gitfakemmap(void *start, size_t length, int prot , int flags, int fd, off_t offset) +void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset) { - int n = 0; - off_t current_offset = lseek(fd, 0, SEEK_CUR); + size_t n = 0; if (start != NULL || !(flags & MAP_PRIVATE)) - die("Invalid usage of gitfakemmap."); - - if (lseek(fd, offset, SEEK_SET) < 0) { - errno = EINVAL; - return MAP_FAILED; - } + die("Invalid usage of mmap when built with NO_MMAP"); start = xmalloc(length); if (start == NULL) { @@ -20,14 +14,16 @@ void *gitfakemmap(void *start, size_t length, int prot , int flags, int fd, off_ } while (n < length) { - int count = read(fd, start+n, length-n); + ssize_t count = pread(fd, (char *)start + n, length - n, offset + n); if (count == 0) { - memset(start+n, 0, length-n); + memset((char *)start+n, 0, length-n); break; } if (count < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; free(start); errno = EACCES; return MAP_FAILED; @@ -36,15 +32,10 @@ void *gitfakemmap(void *start, size_t length, int prot , int flags, int fd, off_ n += count; } - if (current_offset != lseek(fd, current_offset, SEEK_SET)) { - errno = EINVAL; - return MAP_FAILED; - } - return start; } -int gitfakemunmap(void *start, size_t length) +int git_munmap(void *start, size_t length) { free(start); return 0; @@ -349,10 +349,10 @@ int git_config(config_fn_t fn) * $GIT_CONFIG_LOCAL will make it process it in addition to the * global config file, the same way it would the per-repository * config file otherwise. */ - filename = getenv("GIT_CONFIG"); + filename = getenv(CONFIG_ENVIRONMENT); if (!filename) { home = getenv("HOME"); - filename = getenv("GIT_CONFIG_LOCAL"); + filename = getenv(CONFIG_LOCAL_ENVIRONMENT); if (!filename) filename = repo_config = xstrdup(git_path("config")); } @@ -543,9 +543,9 @@ int git_config_set_multivar(const char* key, const char* value, char* lock_file; const char* last_dot = strrchr(key, '.'); - config_filename = getenv("GIT_CONFIG"); + config_filename = getenv(CONFIG_ENVIRONMENT); if (!config_filename) { - config_filename = getenv("GIT_CONFIG_LOCAL"); + config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT); if (!config_filename) config_filename = git_path("config"); } @@ -753,9 +753,9 @@ int git_config_rename_section(const char *old_name, const char *new_name) int out_fd; char buf[1024]; - config_filename = getenv("GIT_CONFIG"); + config_filename = getenv(CONFIG_ENVIRONMENT); if (!config_filename) { - config_filename = getenv("GIT_CONFIG_LOCAL"); + config_filename = getenv(CONFIG_LOCAL_ENVIRONMENT); if (!config_filename) config_filename = git_path("config"); } diff --git a/configure.ac b/configure.ac index e153d53823..7cfb3a0666 100644 --- a/configure.ac +++ b/configure.ac @@ -235,9 +235,6 @@ AC_SUBST(NO_SETENV) # # Define NO_SYMLINK_HEAD if you never want .git/HEAD to be a symbolic link. # Enable it on Windows. By default, symrefs are still used. -# -# Define NO_ACCURATE_DIFF if your diff program at least sometimes misses -# a missing newline at the end of the file. ## Site configuration (override autodetection) ## --with-PACKAGE[=ARG] and --without-PACKAGE diff --git a/contrib/emacs/vc-git.el b/contrib/emacs/vc-git.el index 8b6361922f..3eb4bd19e9 100644 --- a/contrib/emacs/vc-git.el +++ b/contrib/emacs/vc-git.el @@ -58,8 +58,9 @@ (with-temp-buffer (let* ((dir (file-name-directory file)) (name (file-relative-name file dir))) - (when dir (cd dir)) - (and (ignore-errors (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name))) + (and (ignore-errors + (when dir (cd dir)) + (eq 0 (call-process "git" nil '(t nil) nil "ls-files" "-c" "-z" "--" name))) (let ((str (buffer-string))) (and (> (length str) (length name)) (string= (substring str 0 (1+ (length name))) (concat name "\0")))))))) @@ -860,8 +860,6 @@ static void checkdiff_consume(void *priv, char *line, unsigned long len) if (line[0] == '+') { int i, spaces = 0; - data->lineno++; - /* check space before tab */ for (i = 1; i < len && (line[i] == ' ' || line[i] == '\t'); i++) if (line[i] == ' ') @@ -876,6 +874,8 @@ static void checkdiff_consume(void *priv, char *line, unsigned long len) if (isspace(line[len - 1])) printf("%s:%d: white space at end: %.*s\n", data->filename, data->lineno, (int)len, line); + + data->lineno++; } else if (line[0] == ' ') data->lineno++; else if (line[0] == '@') { diff --git a/exec_cmd.c b/exec_cmd.c index 5d6a1247b4..3996bce33f 100644 --- a/exec_cmd.c +++ b/exec_cmd.c @@ -21,7 +21,7 @@ const char *git_exec_path(void) if (current_exec_path) return current_exec_path; - env = getenv("GIT_EXEC_PATH"); + env = getenv(EXEC_PATH_ENVIRONMENT); if (env && *env) { return env; } @@ -35,7 +35,7 @@ int execv_git_cmd(const char **argv) char git_command[PATH_MAX + 1]; int i; const char *paths[] = { current_exec_path, - getenv("GIT_EXEC_PATH"), + getenv(EXEC_PATH_ENVIRONMENT), builtin_exec_path }; for (i = 0; i < ARRAY_SIZE(paths); ++i) { diff --git a/git-add--interactive.perl b/git-add--interactive.perl new file mode 100755 index 0000000000..0057f86588 --- /dev/null +++ b/git-add--interactive.perl @@ -0,0 +1,804 @@ +#!/usr/bin/perl -w + + +use strict; + +sub run_cmd_pipe { + my $fh = undef; + open($fh, '-|', @_) or die; + return <$fh>; +} + +my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir)); + +if (!defined $GIT_DIR) { + exit(1); # rev-parse would have already said "not a git repo" +} +chomp($GIT_DIR); + +sub refresh { + my $fh; + open $fh, '-|', qw(git update-index --refresh) + or die; + while (<$fh>) { + ;# ignore 'needs update' + } + close $fh; +} + +sub list_untracked { + map { + chomp $_; + $_; + } + run_cmd_pipe(qw(git ls-files --others + --exclude-per-directory=.gitignore), + "--exclude-from=$GIT_DIR/info/exclude", + '--', @_); +} + +my $status_fmt = '%12s %12s %s'; +my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path'); + +# Returns list of hashes, contents of each of which are: +# PRINT: print message +# VALUE: pathname +# BINARY: is a binary path +# INDEX: is index different from HEAD? +# FILE: is file different from index? +# INDEX_ADDDEL: is it add/delete between HEAD and index? +# FILE_ADDDEL: is it add/delete between index and file? + +sub list_modified { + my ($only) = @_; + my (%data, @return); + my ($add, $del, $adddel, $file); + + for (run_cmd_pipe(qw(git diff-index --cached + --numstat --summary HEAD))) { + if (($add, $del, $file) = + /^([-\d]+) ([-\d]+) (.*)/) { + my ($change, $bin); + if ($add eq '-' && $del eq '-') { + $change = 'binary'; + $bin = 1; + } + else { + $change = "+$add/-$del"; + } + $data{$file} = { + INDEX => $change, + BINARY => $bin, + FILE => 'nothing', + } + } + elsif (($adddel, $file) = + /^ (create|delete) mode [0-7]+ (.*)$/) { + $data{$file}{INDEX_ADDDEL} = $adddel; + } + } + + for (run_cmd_pipe(qw(git diff-files --numstat --summary))) { + if (($add, $del, $file) = + /^([-\d]+) ([-\d]+) (.*)/) { + if (!exists $data{$file}) { + $data{$file} = +{ + INDEX => 'unchanged', + BINARY => 0, + }; + } + my ($change, $bin); + if ($add eq '-' && $del eq '-') { + $change = 'binary'; + $bin = 1; + } + else { + $change = "+$add/-$del"; + } + $data{$file}{FILE} = $change; + if ($bin) { + $data{$file}{BINARY} = 1; + } + } + elsif (($adddel, $file) = + /^ (create|delete) mode [0-7]+ (.*)$/) { + $data{$file}{FILE_ADDDEL} = $adddel; + } + } + + for (sort keys %data) { + my $it = $data{$_}; + + if ($only) { + if ($only eq 'index-only') { + next if ($it->{INDEX} eq 'unchanged'); + } + if ($only eq 'file-only') { + next if ($it->{FILE} eq 'nothing'); + } + } + push @return, +{ + VALUE => $_, + PRINT => (sprintf $status_fmt, + $it->{INDEX}, $it->{FILE}, $_), + %$it, + }; + } + return @return; +} + +sub find_unique { + my ($string, @stuff) = @_; + my $found = undef; + for (my $i = 0; $i < @stuff; $i++) { + my $it = $stuff[$i]; + my $hit = undef; + if (ref $it) { + if ((ref $it) eq 'ARRAY') { + $it = $it->[0]; + } + else { + $it = $it->{VALUE}; + } + } + eval { + if ($it =~ /^$string/) { + $hit = 1; + }; + }; + if (defined $hit && defined $found) { + return undef; + } + if ($hit) { + $found = $i + 1; + } + } + return $found; +} + +sub list_and_choose { + my ($opts, @stuff) = @_; + my (@chosen, @return); + my $i; + + TOPLOOP: + while (1) { + my $last_lf = 0; + + if ($opts->{HEADER}) { + if (!$opts->{LIST_FLAT}) { + print " "; + } + print "$opts->{HEADER}\n"; + } + for ($i = 0; $i < @stuff; $i++) { + my $chosen = $chosen[$i] ? '*' : ' '; + my $print = $stuff[$i]; + if (ref $print) { + if ((ref $print) eq 'ARRAY') { + $print = $print->[0]; + } + else { + $print = $print->{PRINT}; + } + } + printf("%s%2d: %s", $chosen, $i+1, $print); + if (($opts->{LIST_FLAT}) && + (($i + 1) % ($opts->{LIST_FLAT}))) { + print "\t"; + $last_lf = 0; + } + else { + print "\n"; + $last_lf = 1; + } + } + if (!$last_lf) { + print "\n"; + } + + return if ($opts->{LIST_ONLY}); + + print $opts->{PROMPT}; + if ($opts->{SINGLETON}) { + print "> "; + } + else { + print ">> "; + } + my $line = <STDIN>; + last if (!$line); + chomp $line; + my $donesomething = 0; + for my $choice (split(/[\s,]+/, $line)) { + my $choose = 1; + my ($bottom, $top); + + # Input that begins with '-'; unchoose + if ($choice =~ s/^-//) { + $choose = 0; + } + # A range can be specified like 5-7 + if ($choice =~ /^(\d+)-(\d+)$/) { + ($bottom, $top) = ($1, $2); + } + elsif ($choice =~ /^\d+$/) { + $bottom = $top = $choice; + } + elsif ($choice eq '*') { + $bottom = 1; + $top = 1 + @stuff; + } + else { + $bottom = $top = find_unique($choice, @stuff); + if (!defined $bottom) { + print "Huh ($choice)?\n"; + next TOPLOOP; + } + } + if ($opts->{SINGLETON} && $bottom != $top) { + print "Huh ($choice)?\n"; + next TOPLOOP; + } + for ($i = $bottom-1; $i <= $top-1; $i++) { + next if (@stuff <= $i); + $chosen[$i] = $choose; + $donesomething++; + } + } + last if (!$donesomething || $opts->{IMMEDIATE}); + } + for ($i = 0; $i < @stuff; $i++) { + if ($chosen[$i]) { + push @return, $stuff[$i]; + } + } + return @return; +} + +sub status_cmd { + list_and_choose({ LIST_ONLY => 1, HEADER => $status_head }, + list_modified()); + print "\n"; +} + +sub say_n_paths { + my $did = shift @_; + my $cnt = scalar @_; + print "$did "; + if (1 < $cnt) { + print "$cnt paths\n"; + } + else { + print "one path\n"; + } +} + +sub update_cmd { + my @mods = list_modified('file-only'); + return if (!@mods); + + my @update = list_and_choose({ PROMPT => 'Update', + HEADER => $status_head, }, + @mods); + if (@update) { + system(qw(git update-index --add --), + map { $_->{VALUE} } @update); + say_n_paths('updated', @update); + } + print "\n"; +} + +sub revert_cmd { + my @update = list_and_choose({ PROMPT => 'Revert', + HEADER => $status_head, }, + list_modified()); + if (@update) { + my @lines = run_cmd_pipe(qw(git ls-tree HEAD --), + map { $_->{VALUE} } @update); + my $fh; + open $fh, '|-', qw(git update-index --index-info) + or die; + for (@lines) { + print $fh $_; + } + close($fh); + for (@update) { + if ($_->{INDEX_ADDDEL} && + $_->{INDEX_ADDDEL} eq 'create') { + system(qw(git update-index --force-remove --), + $_->{VALUE}); + print "note: $_->{VALUE} is untracked now.\n"; + } + } + refresh(); + say_n_paths('reverted', @update); + } + print "\n"; +} + +sub add_untracked_cmd { + my @add = list_and_choose({ PROMPT => 'Add untracked' }, + list_untracked()); + if (@add) { + system(qw(git update-index --add --), @add); + say_n_paths('added', @add); + } + print "\n"; +} + +sub parse_diff { + my ($path) = @_; + my @diff = run_cmd_pipe(qw(git diff-files -p --), $path); + my (@hunk) = { TEXT => [] }; + + for (@diff) { + if (/^@@ /) { + push @hunk, { TEXT => [] }; + } + push @{$hunk[-1]{TEXT}}, $_; + } + return @hunk; +} + +sub hunk_splittable { + my ($text) = @_; + + my @s = split_hunk($text); + return (1 < @s); +} + +sub parse_hunk_header { + my ($line) = @_; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + $line =~ /^@@ -(\d+)(?:,(\d+)) \+(\d+)(?:,(\d+)) @@/; + return ($o_ofs, $o_cnt, $n_ofs, $n_cnt); +} + +sub split_hunk { + my ($text) = @_; + my @split = (); + + # If there are context lines in the middle of a hunk, + # it can be split, but we would need to take care of + # overlaps later. + + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]); + my $hunk_start = 1; + my $next_hunk_start; + + OUTER: + while (1) { + my $next_hunk_start = undef; + my $i = $hunk_start - 1; + my $this = +{ + TEXT => [], + OLD => $o_ofs, + NEW => $n_ofs, + OCNT => 0, + NCNT => 0, + ADDDEL => 0, + POSTCTX => 0, + }; + + while (++$i < @$text) { + my $line = $text->[$i]; + if ($line =~ /^ /) { + if ($this->{ADDDEL} && + !defined $next_hunk_start) { + # We have seen leading context and + # adds/dels and then here is another + # context, which is trailing for this + # split hunk and leading for the next + # one. + $next_hunk_start = $i; + } + push @{$this->{TEXT}}, $line; + $this->{OCNT}++; + $this->{NCNT}++; + if (defined $next_hunk_start) { + $this->{POSTCTX}++; + } + next; + } + + # add/del + if (defined $next_hunk_start) { + # We are done with the current hunk and + # this is the first real change for the + # next split one. + $hunk_start = $next_hunk_start; + $o_ofs = $this->{OLD} + $this->{OCNT}; + $n_ofs = $this->{NEW} + $this->{NCNT}; + $o_ofs -= $this->{POSTCTX}; + $n_ofs -= $this->{POSTCTX}; + push @split, $this; + redo OUTER; + } + push @{$this->{TEXT}}, $line; + $this->{ADDDEL}++; + if ($line =~ /^-/) { + $this->{OCNT}++; + } + else { + $this->{NCNT}++; + } + } + + push @split, $this; + last; + } + + for my $hunk (@split) { + $o_ofs = $hunk->{OLD}; + $n_ofs = $hunk->{NEW}; + $o_cnt = $hunk->{OCNT}; + $n_cnt = $hunk->{NCNT}; + + my $head = ("@@ -$o_ofs" . + (($o_cnt != 1) ? ",$o_cnt" : '') . + " +$n_ofs" . + (($n_cnt != 1) ? ",$n_cnt" : '') . + " @@\n"); + unshift @{$hunk->{TEXT}}, $head; + } + return map { $_->{TEXT} } @split; +} + +sub find_last_o_ctx { + my ($it) = @_; + my $text = $it->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]); + my $i = @{$text}; + my $last_o_ctx = $o_ofs + $o_cnt; + while (0 < --$i) { + my $line = $text->[$i]; + if ($line =~ /^ /) { + $last_o_ctx--; + next; + } + last; + } + return $last_o_ctx; +} + +sub merge_hunk { + my ($prev, $this) = @_; + my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) = + parse_hunk_header($prev->{TEXT}[0]); + my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) = + parse_hunk_header($this->{TEXT}[0]); + + my (@line, $i, $ofs, $o_cnt, $n_cnt); + $ofs = $o0_ofs; + $o_cnt = $n_cnt = 0; + for ($i = 1; $i < @{$prev->{TEXT}}; $i++) { + my $line = $prev->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + + last if ($o1_ofs <= $ofs); + + $o_cnt++; + $ofs++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + + for ($i = 1; $i < @{$this->{TEXT}}; $i++) { + my $line = $this->{TEXT}[$i]; + if ($line =~ /^\+/) { + $n_cnt++; + push @line, $line; + next; + } + $ofs++; + $o_cnt++; + if ($line =~ /^ /) { + $n_cnt++; + } + push @line, $line; + } + my $head = ("@@ -$o0_ofs" . + (($o_cnt != 1) ? ",$o_cnt" : '') . + " +$n0_ofs" . + (($n_cnt != 1) ? ",$n_cnt" : '') . + " @@\n"); + @{$prev->{TEXT}} = ($head, @line); +} + +sub coalesce_overlapping_hunks { + my (@in) = @_; + my @out = (); + + my ($last_o_ctx); + + for (grep { $_->{USE} } @in) { + my $text = $_->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + parse_hunk_header($text->[0]); + if (defined $last_o_ctx && + $o_ofs <= $last_o_ctx) { + merge_hunk($out[-1], $_); + } + else { + push @out, $_; + } + $last_o_ctx = find_last_o_ctx($out[-1]); + } + return @out; +} + +sub help_patch_cmd { + print <<\EOF ; +y - stage this hunk +n - do not stage this hunk +a - stage this and all the remaining hunks +d - do not stage this hunk nor any of the remaining hunks +j - leave this hunk undecided, see next undecided hunk +J - leave this hunk undecided, see next hunk +k - leave this hunk undecided, see previous undecided hunk +K - leave this hunk undecided, see previous hunk +s - split the current hunk into smaller hunks +EOF +} + +sub patch_update_cmd { + my @mods = list_modified('file-only'); + @mods = grep { !($_->{BINARY}) } @mods; + return if (!@mods); + + my ($it) = list_and_choose({ PROMPT => 'Patch update', + SINGLETON => 1, + IMMEDIATE => 1, + HEADER => $status_head, }, + @mods); + return if (!$it); + + my ($ix, $num); + my $path = $it->{VALUE}; + my ($head, @hunk) = parse_diff($path); + for (@{$head->{TEXT}}) { + print; + } + $num = scalar @hunk; + $ix = 0; + + while (1) { + my ($prev, $next, $other, $undecided, $i); + $other = ''; + + if ($num <= $ix) { + $ix = 0; + } + for ($i = 0; $i < $ix; $i++) { + if (!defined $hunk[$i]{USE}) { + $prev = 1; + $other .= '/k'; + last; + } + } + if ($ix) { + $other .= '/K'; + } + for ($i = $ix + 1; $i < $num; $i++) { + if (!defined $hunk[$i]{USE}) { + $next = 1; + $other .= '/j'; + last; + } + } + if ($ix < $num - 1) { + $other .= '/J'; + } + for ($i = 0; $i < $num; $i++) { + if (!defined $hunk[$i]{USE}) { + $undecided = 1; + last; + } + } + last if (!$undecided); + + if (hunk_splittable($hunk[$ix]{TEXT})) { + $other .= '/s'; + } + for (@{$hunk[$ix]{TEXT}}) { + print; + } + print "Stage this hunk [y/n/a/d$other/?]? "; + my $line = <STDIN>; + if ($line) { + if ($line =~ /^y/i) { + $hunk[$ix]{USE} = 1; + } + elsif ($line =~ /^n/i) { + $hunk[$ix]{USE} = 0; + } + elsif ($line =~ /^a/i) { + while ($ix < $num) { + if (!defined $hunk[$ix]{USE}) { + $hunk[$ix]{USE} = 1; + } + $ix++; + } + next; + } + elsif ($line =~ /^d/i) { + while ($ix < $num) { + if (!defined $hunk[$ix]{USE}) { + $hunk[$ix]{USE} = 0; + } + $ix++; + } + next; + } + elsif ($other =~ /K/ && $line =~ /^K/) { + $ix--; + next; + } + elsif ($other =~ /J/ && $line =~ /^J/) { + $ix++; + next; + } + elsif ($other =~ /k/ && $line =~ /^k/) { + while (1) { + $ix--; + last if (!$ix || + !defined $hunk[$ix]{USE}); + } + next; + } + elsif ($other =~ /j/ && $line =~ /^j/) { + while (1) { + $ix++; + last if ($ix >= $num || + !defined $hunk[$ix]{USE}); + } + next; + } + elsif ($other =~ /s/ && $line =~ /^s/) { + my @split = split_hunk($hunk[$ix]{TEXT}); + if (1 < @split) { + print "Split into ", + scalar(@split), " hunks.\n"; + } + splice(@hunk, $ix, 1, + map { +{ TEXT => $_, USE => undef } } + @split); + $num = scalar @hunk; + next; + } + else { + help_patch_cmd($other); + next; + } + # soft increment + while (1) { + $ix++; + last if ($ix >= $num || + !defined $hunk[$ix]{USE}); + } + } + } + + @hunk = coalesce_overlapping_hunks(@hunk); + + my ($o_lofs, $n_lofs) = (0, 0); + my @result = (); + for (@hunk) { + my $text = $_->{TEXT}; + my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = + parse_hunk_header($text->[0]); + + if (!$_->{USE}) { + if (!defined $o_cnt) { $o_cnt = 1; } + if (!defined $n_cnt) { $n_cnt = 1; } + + # We would have added ($n_cnt - $o_cnt) lines + # to the postimage if we were to use this hunk, + # but we didn't. So the line number that the next + # hunk starts at would be shifted by that much. + $n_lofs -= ($n_cnt - $o_cnt); + next; + } + else { + if ($n_lofs) { + $n_ofs += $n_lofs; + $text->[0] = ("@@ -$o_ofs" . + ((defined $o_cnt) + ? ",$o_cnt" : '') . + " +$n_ofs" . + ((defined $n_cnt) + ? ",$n_cnt" : '') . + " @@\n"); + } + for (@$text) { + push @result, $_; + } + } + } + + if (@result) { + my $fh; + + open $fh, '|-', qw(git apply --cached); + for (@{$head->{TEXT}}, @result) { + print $fh $_; + } + if (!close $fh) { + for (@{$head->{TEXT}}, @result) { + print STDERR $_; + } + } + refresh(); + } + + print "\n"; +} + +sub diff_cmd { + my @mods = list_modified('index-only'); + @mods = grep { !($_->{BINARY}) } @mods; + return if (!@mods); + my (@them) = list_and_choose({ PROMPT => 'Review diff', + IMMEDIATE => 1, + HEADER => $status_head, }, + @mods); + return if (!@them); + system(qw(git diff-index -p --cached HEAD --), + map { $_->{VALUE} } @them); +} + +sub quit_cmd { + print "Bye.\n"; + exit(0); +} + +sub help_cmd { + print <<\EOF ; +status - show paths with changes +update - add working tree state to the staged set of changes +revert - revert staged set of changes back to the HEAD version +patch - pick hunks and update selectively +diff - view diff between HEAD and index +add untracked - add contents of untracked files to the staged set of changes +EOF +} + +sub main_loop { + my @cmd = ([ 'status', \&status_cmd, ], + [ 'update', \&update_cmd, ], + [ 'revert', \&revert_cmd, ], + [ 'add untracked', \&add_untracked_cmd, ], + [ 'patch', \&patch_update_cmd, ], + [ 'diff', \&diff_cmd, ], + [ 'quit', \&quit_cmd, ], + [ 'help', \&help_cmd, ], + ); + while (1) { + my ($it) = list_and_choose({ PROMPT => 'What now', + SINGLETON => 1, + LIST_FLAT => 4, + HEADER => '*** Commands ***', + IMMEDIATE => 1 }, @cmd); + if ($it) { + eval { + $it->[1]->(); + }; + if ($@) { + print "$@"; + } + } + } +} + +my @z; + +refresh(); +status_cmd(); +main_loop(); @@ -401,14 +401,14 @@ do changed="$(git-diff-index --cached --name-only HEAD)" if test '' = "$changed" then - echo "No changes - did you forget update-index?" + echo "No changes - did you forget to use 'git add'?" stop_here_user_resolve $this fi unmerged=$(git-ls-files -u) if test -n "$unmerged" then echo "You still have unmerged paths in your index" - echo "did you forget update-index?" + echo "did you forget to use 'git add'?" stop_here_user_resolve $this fi apply_status=0 diff --git a/git-checkout.sh b/git-checkout.sh index 4192a99fec..92ec069a3a 100755 --- a/git-checkout.sh +++ b/git-checkout.sh @@ -146,8 +146,11 @@ fi [ -z "$branch$newbranch" ] && [ "$new" != "$old" ] && - die "git checkout: to checkout the requested commit you need to specify - a name for a new branch which is created and switched to" + die "git checkout: provided reference cannot be checked out directly + + You need -b to associate a new branch with the wanted checkout. Example: + git checkout -b <new_branch_name> $arg +" if [ "X$old" = X ] then diff --git a/git-clone.sh b/git-clone.sh index 1f5d07a057..490f3e48db 100755 --- a/git-clone.sh +++ b/git-clone.sh @@ -14,7 +14,7 @@ die() { } usage() { - die "Usage: $0 [--template=<template_directory>] [--no-separate-remote] [--reference <reference-repo>] [--bare] [-l [-s]] [-q] [-u <upload-pack>] [--origin <name>] [-n] <repo> [<dir>]" + die "Usage: $0 [--template=<template_directory>] [--reference <reference-repo>] [--bare] [-l [-s]] [-q] [-u <upload-pack>] [--origin <name>] [-n] <repo> [<dir>]" } get_repo_base() { @@ -137,11 +137,9 @@ while *,--template=*) template="$1" ;; *,-q|*,--quiet) quiet=-q ;; - *,--use-separate-remote) - # default - use_separate_remote=t ;; + *,--use-separate-remote) ;; *,--no-separate-remote) - use_separate_remote= ;; + die "clones are always made with separate-remote layout" ;; 1,--reference) usage ;; *,--reference) shift; reference="$1" ;; @@ -327,12 +325,8 @@ cd "$D" || exit if test -z "$bare" && test -f "$GIT_DIR/REMOTE_HEAD" then - # Figure out which remote branch HEAD points at. - case "$use_separate_remote" in - '') remote_top=refs/heads ;; - *) remote_top="refs/remotes/$origin" ;; - esac - + # a non-bare repository is always in separate-remote layout + remote_top="refs/remotes/$origin" head_sha1=`cat "$GIT_DIR/REMOTE_HEAD"` case "$head_sha1" in 'ref: refs/'*) @@ -366,41 +360,26 @@ then ) ) - # Write out remotes/$origin file, and update our "$head_points_at". + # Write out remote.$origin config, and update our "$head_points_at". case "$head_points_at" in ?*) - mkdir -p "$GIT_DIR/remotes" && + # Local default branch git-symbolic-ref HEAD "refs/heads/$head_points_at" && - case "$use_separate_remote" in - t) origin_track="$remote_top/$head_points_at" - git-update-ref HEAD "$head_sha1" ;; - *) origin_track="$remote_top/$origin" - git-update-ref "refs/heads/$origin" "$head_sha1" ;; - esac && + + # Tracking branch for the primary branch at the remote. + origin_track="$remote_top/$head_points_at" && + git-update-ref HEAD "$head_sha1" && + + # Upstream URL git-repo-config remote."$origin".url "$repo" && + + # Set up the mappings to track the remote branches. git-repo-config remote."$origin".fetch \ - "refs/heads/$head_points_at:$origin_track" && - (cd "$GIT_DIR/$remote_top" && find . -type f -print) | - while read dotslref - do - name=`expr "$dotslref" : './\(.*\)'` - if test "z$head_points_at" = "z$name" - then - continue - fi - if test "$use_separate_remote" = '' && - test "z$origin" = "z$name" - then - continue - fi - git-repo-config remote."$origin".fetch "refs/heads/${name}:$remote_top/${name}" '^$' - done && - case "$use_separate_remote" in - t) - rm -f "refs/remotes/$origin/HEAD" - git-symbolic-ref "refs/remotes/$origin/HEAD" \ - "refs/remotes/$origin/$head_points_at" - esac && + "refs/heads/*:$remote_top/*" '^$' && + rm -f "refs/remotes/$origin/HEAD" + git-symbolic-ref "refs/remotes/$origin/HEAD" \ + "refs/remotes/$origin/$head_points_at" && + git-repo-config branch."$head_points_at".remote "$origin" && git-repo-config branch."$head_points_at".merge "refs/heads/$head_points_at" esac diff --git a/git-compat-util.h b/git-compat-util.h index bc296b3a45..5d9eb2615b 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -11,8 +11,10 @@ #define ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0])) +#if !defined(__APPLE__) && !defined(__FreeBSD__) #define _XOPEN_SOURCE 600 /* glibc2 and AIX 5.3L need 500, OpenBSD needs 600 for S_ISLNK() */ #define _XOPEN_SOURCE_EXTENDED 1 /* AIX 5.3L needs this */ +#endif #define _GNU_SOURCE #define _BSD_SOURCE @@ -69,10 +71,12 @@ extern void usage(const char *err) NORETURN; extern void die(const char *err, ...) NORETURN __attribute__((format (printf, 1, 2))); extern int error(const char *err, ...) __attribute__((format (printf, 1, 2))); +extern void warn(const char *err, ...) __attribute__((format (printf, 1, 2))); extern void set_usage_routine(void (*routine)(const char *err) NORETURN); extern void set_die_routine(void (*routine)(const char *err, va_list params) NORETURN); extern void set_error_routine(void (*routine)(const char *err, va_list params)); +extern void set_warn_routine(void (*routine)(const char *warn, va_list params)); #ifdef NO_MMAP @@ -83,10 +87,10 @@ extern void set_error_routine(void (*routine)(const char *err, va_list params)); #define MAP_FAILED ((void*)-1) #endif -#define mmap gitfakemmap -#define munmap gitfakemunmap -extern void *gitfakemmap(void *start, size_t length, int prot , int flags, int fd, off_t offset); -extern int gitfakemunmap(void *start, size_t length); +#define mmap git_mmap +#define munmap git_munmap +extern void *git_mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset); +extern int git_munmap(void *start, size_t length); #else /* NO_MMAP */ diff --git a/git-merge-one-file.sh b/git-merge-one-file.sh index c49e4c65af..7d62d7902c 100755 --- a/git-merge-one-file.sh +++ b/git-merge-one-file.sh @@ -104,7 +104,7 @@ case "${1:-.}${2:-.}${3:-.}" in # Be careful for funny filename such as "-L" in "$4", which # would confuse "merge" greatly. src1=`git-unpack-file $2` - merge "$src1" "$orig" "$src2" + git-merge-file "$src1" "$orig" "$src2" ret=$? # Create the working tree file, using "our tree" version from the diff --git a/git-merge.sh b/git-merge.sh index 4ebfcf65d9..7dd0a11236 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -32,7 +32,7 @@ savestate() { restorestate() { if test -f "$GIT_DIR/MERGE_SAVE" then - git reset --hard $head + git reset --hard $head >/dev/null cpio -iuv <"$GIT_DIR/MERGE_SAVE" git-update-index --refresh >/dev/null fi @@ -221,6 +221,8 @@ do remotehead=$(git-rev-parse --verify "$remote"^0 2>/dev/null) || die "$remote - not something we can merge" remoteheads="${remoteheads}$remotehead " + eval GITHEAD_$remotehead='"$remote"' + export GITHEAD_$remotehead done set x $remoteheads ; shift diff --git a/git-parse-remote.sh b/git-parse-remote.sh index bc881cc5f9..aaef861ada 100755 --- a/git-parse-remote.sh +++ b/git-parse-remote.sh @@ -7,18 +7,7 @@ GIT_DIR=$(git-rev-parse --git-dir 2>/dev/null) || :; get_data_source () { case "$1" in */*) - # Not so fast. This could be the partial URL shorthand... - token=$(expr "z$1" : 'z\([^/]*\)/') - remainder=$(expr "z$1" : 'z[^/]*/\(.*\)') - if test "$(git-repo-config --get "remote.$token.url")" - then - echo config-partial - elif test -f "$GIT_DIR/branches/$token" - then - echo branches-partial - else - echo '' - fi + echo '' ;; *) if test "$(git-repo-config --get "remote.$1.url")" @@ -40,12 +29,7 @@ get_remote_url () { data_source=$(get_data_source "$1") case "$data_source" in '') - echo "$1" ;; - config-partial) - token=$(expr "z$1" : 'z\([^/]*\)/') - remainder=$(expr "z$1" : 'z[^/]*/\(.*\)') - url=$(git-repo-config --get "remote.$token.url") - echo "$url/$remainder" + echo "$1" ;; config) git-repo-config --get "remote.$1.url" @@ -54,14 +38,10 @@ get_remote_url () { sed -ne '/^URL: */{ s///p q - }' "$GIT_DIR/remotes/$1" ;; + }' "$GIT_DIR/remotes/$1" + ;; branches) - sed -e 's/#.*//' "$GIT_DIR/branches/$1" ;; - branches-partial) - token=$(expr "z$1" : 'z\([^/]*\)/') - remainder=$(expr "z$1" : 'z[^/]*/\(.*\)') - url=$(sed -e 's/#.*//' "$GIT_DIR/branches/$token") - echo "$url/$remainder" + sed -e 's/#.*//' "$GIT_DIR/branches/$1" ;; *) die "internal error: get-remote-url $1" ;; @@ -77,7 +57,7 @@ get_default_remote () { get_remote_default_refs_for_push () { data_source=$(get_data_source "$1") case "$data_source" in - '' | config-partial | branches | branches-partial) + '' | branches) ;; # no default push mapping, just send matching refs. config) git-repo-config --get-all "remote.$1.push" ;; @@ -132,11 +112,12 @@ canon_refs_list_for_fetch () { # or the first one otherwise; add prefix . to the rest # to prevent the secondary branches to be merged by default. merge_branches= - found_mergeref= curr_branch= if test "$1" = "-d" then shift ; remote="$1" ; shift + set x $(expand_refs_wildcard "$@") + shift if test "$remote" = "$(get_default_remote)" then curr_branch=$(git-symbolic-ref HEAD | \ @@ -144,8 +125,6 @@ canon_refs_list_for_fetch () { merge_branches=$(git-repo-config \ --get-all "branch.${curr_branch}.merge") fi - set x $(expand_refs_wildcard "$@") - shift fi for ref do @@ -171,10 +150,6 @@ canon_refs_list_for_fetch () { dot_prefix= && break done fi - if test -z $dot_prefix - then - found_mergeref=true - fi case "$remote" in '') remote=HEAD ;; refs/heads/* | refs/tags/* | refs/remotes/*) ;; @@ -195,18 +170,13 @@ canon_refs_list_for_fetch () { fi echo "${dot_prefix}${force}${remote}:${local}" done - if test -z "$found_mergeref" -a "$curr_branch" - then - echo >&2 "Warning: No merge candidate found because value of config option - \"branch.${curr_branch}.merge\" does not match any remote branch fetched." - fi } # Returns list of src: (no store), or src:dst (store) get_remote_default_refs_for_fetch () { data_source=$(get_data_source "$1") case "$data_source" in - '' | config-partial | branches-partial) + '') echo "HEAD:" ;; config) canon_refs_list_for_fetch -d "$1" \ diff --git a/git-pull.sh b/git-pull.sh index e23beb685d..1703091bbb 100755 --- a/git-pull.sh +++ b/git-pull.sh @@ -76,6 +76,10 @@ merge_head=$(sed -e '/ not-for-merge /d' \ case "$merge_head" in '') + curr_branch=$(git-symbolic-ref HEAD | \ + sed -e 's|^refs/heads/||') + echo >&2 "Warning: No merge candidate found because value of config option + \"branch.${curr_branch}.merge\" does not match any remote branch fetched." echo >&2 "No changes." exit 0 ;; diff --git a/git-rebase.sh b/git-rebase.sh index 2b4f3477fa..ece31425d0 100755 --- a/git-rebase.sh +++ b/git-rebase.sh @@ -292,6 +292,7 @@ then fi # Rewind the head to "$onto"; this saves our current head in ORIG_HEAD. +echo "First, rewinding head to replay your work on top of it..." git-reset --hard "$onto" # If the $onto is a proper descendant of the tip of the branch, then diff --git a/git-reset.sh b/git-reset.sh index 8d95e3748d..2379db082f 100755 --- a/git-reset.sh +++ b/git-reset.sh @@ -86,7 +86,12 @@ update_ref_status=$? case "$reset_type" in --hard ) - ;; # Nothing else to do + test $update_ref_status = 0 && { + echo -n "HEAD is now at " + GIT_PAGER= git log --max-count=1 --pretty=oneline \ + --abbrev-commit HEAD + } + ;; --soft ) ;; # Nothing else to do --mixed ) diff --git a/git-revert.sh b/git-revert.sh index 6eab3c72df..50cc47b063 100755 --- a/git-revert.sh +++ b/git-revert.sh @@ -155,7 +155,7 @@ Conflicts: uniq } >>"$GIT_DIR/MERGE_MSG" echo >&2 "Automatic $me failed. After resolving the conflicts," - echo >&2 "mark the corrected paths with 'git-update-index <paths>'" + echo >&2 "mark the corrected paths with 'git-add <paths>'" echo >&2 "and commit the result." case "$me" in cherry-pick) diff --git a/git-svn.perl b/git-svn.perl index 73ab8d873f..4288a05c16 100755 --- a/git-svn.perl +++ b/git-svn.perl @@ -32,40 +32,30 @@ my %SKIP = ( 'svn:wc:ra_dav:version-url' => 1, ); sub fatal (@) { print STDERR @_; exit 1 } -# If SVN:: library support is added, please make the dependencies -# optional and preserve the capability to use the command-line client. -# use eval { require SVN::... } to make it lazy load -# We don't use any modules not in the standard Perl distribution: +require SVN::Core; # use()-ing this causes segfaults for me... *shrug* +require SVN::Ra; +require SVN::Delta; +if ($SVN::Core::VERSION lt '1.1.0') { + fatal "Need SVN::Core 1.1.0 or better (got $SVN::Core::VERSION)\n"; +} +push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; +push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor'; +*SVN::Git::Fetcher::process_rm = *process_rm; use Carp qw/croak/; use IO::File qw//; use File::Basename qw/dirname basename/; use File::Path qw/mkpath/; use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev pass_through/; -use File::Spec qw//; -use File::Copy qw/copy/; use POSIX qw/strftime/; use IPC::Open3; use Memoize; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; memoize('revisions_eq'); memoize('cmt_metadata'); memoize('get_commit_time'); -my ($SVN, $_use_lib); - -sub nag_lib { - print STDERR <<EOF; -! Please consider installing the SVN Perl libraries (version 1.1.0 or -! newer). You will generally get better performance and fewer bugs, -! especially if you: -! 1) have a case-insensitive filesystem -! 2) replace symlinks with files (and vice-versa) in commits - -EOF -} - -$_use_lib = 1 unless $ENV{GIT_SVN_NO_LIB}; -libsvn_load(); -nag_lib() unless $_use_lib; +my ($SVN); my $_optimize_commits = 1 unless $ENV{GIT_SVN_NO_OPTIMIZE_COMMITS}; my $sha1 = qr/[a-f\d]{40}/; @@ -82,7 +72,7 @@ my ($_revision,$_stdin,$_no_ignore_ext,$_no_stop_copy,$_help,$_rmdir,$_edit, $_username, $_config_dir, $_no_auth_cache, $_xfer_delta, $_pager, $_color); my (@_branch_from, %tree_map, %users, %rusers, %equiv); -my ($_svn_co_url_revs, $_svn_pg_peg_revs, $_svn_can_do_switch); +my ($_svn_can_do_switch); my @repo_path_split_cache; my %fc_opts = ( 'no-ignore-externals' => \$_no_ignore_ext, @@ -117,7 +107,12 @@ my %cmd = ( init => [ \&init, "Initialize a repo for tracking" . " (requires URL argument)", \%init_opts ], - commit => [ \&commit, "Commit git revisions to SVN", + dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream', + { 'merge|m|M' => \$_merge, + 'strategy|s=s' => \$_strategy, + 'dry-run|n' => \$_dry_run, + %cmt_opts, %fc_opts } ], + 'set-tree' => [ \&commit, "Set an SVN repository to a git tree-ish", { 'stdin|' => \$_stdin, %cmt_opts, %fc_opts, } ], 'show-ignore' => [ \&show_ignore, "Show svn:ignore listings", { 'revision|r=i' => \$_revision } ], @@ -160,11 +155,6 @@ my %cmd = ( 'file|F=s' => \$_file, 'revision|r=s' => \$_revision, %cmt_opts } ], - dcommit => [ \&dcommit, 'Commit several diffs to merge with upstream', - { 'merge|m|M' => \$_merge, - 'strategy|s=s' => \$_strategy, - 'dry-run|n' => \$_dry_run, - %cmt_opts } ], ); my $cmd; @@ -191,7 +181,6 @@ usage(1) unless defined $cmd; init_vars(); load_authors() if $_authors; load_all_refs() if $_branch_all_refs; -svn_compat_check() unless $_use_lib; migration_check() unless $cmd =~ /^(?:init|rebuild|multi-init|commit-diff)$/; $cmd{$cmd}->[0]->(@ARGV); exit 0; @@ -232,28 +221,27 @@ sub version { } sub rebuild { - if (quiet_run(qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0")) { + if (!verify_ref("refs/remotes/$GIT_SVN^0")) { copy_remote_ref(); } $SVN_URL = shift or undef; my $newest_rev = 0; if ($_upgrade) { - sys('git-update-ref',"refs/remotes/$GIT_SVN","$GIT_SVN-HEAD"); + command_noisy('update-ref',"refs/remotes/$GIT_SVN"," + $GIT_SVN-HEAD"); } else { check_upgrade_needed(); } - my $pid = open(my $rev_list,'-|'); - defined $pid or croak $!; - if ($pid == 0) { - exec("git-rev-list","refs/remotes/$GIT_SVN") or croak $!; - } + my ($rev_list, $ctx) = command_output_pipe("rev-list", + "refs/remotes/$GIT_SVN"); my $latest; while (<$rev_list>) { chomp; my $c = $_; croak "Non-SHA1: $c\n" unless $c =~ /^$sha1$/o; - my @commit = grep(/^git-svn-id: /,`git-cat-file commit $c`); + my @commit = grep(/^git-svn-id: /, + command(qw/cat-file commit/, $c)); next if (!@commit); # skip merges my ($url, $rev, $uuid) = extract_metadata($commit[$#commit]); if (!defined $rev || !$uuid) { @@ -279,33 +267,7 @@ sub rebuild { print "r$rev = $c\n"; $newest_rev = $rev if ($rev > $newest_rev); } - close $rev_list or croak $?; - - goto out if $_use_lib; - if (!chdir $SVN_WC) { - svn_cmd_checkout($SVN_URL, $latest, $SVN_WC); - chdir $SVN_WC or croak $!; - } - - $pid = fork; - defined $pid or croak $!; - if ($pid == 0) { - my @svn_up = qw(svn up); - push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - sys(@svn_up,"-r$newest_rev"); - $ENV{GIT_INDEX_FILE} = $GIT_SVN_INDEX; - index_changes(); - exec('git-write-tree') or croak $!; - } - waitpid $pid, 0; - croak $? if $?; -out: - if ($_upgrade) { - print STDERR <<""; -Keeping deprecated refs/head/$GIT_SVN-HEAD for now. Please remove it -when you have upgraded your tools and habits to use refs/remotes/$GIT_SVN - - } + command_close_pipe($rev_list, $ctx); } sub init { @@ -323,10 +285,10 @@ sub init { $SVN_URL = $url; unless (-d $GIT_DIR) { - my @init_db = ('git-init-db'); + my @init_db = ('init-db'); push @init_db, "--template=$_template" if defined $_template; push @init_db, "--shared" if defined $_shared; - sys(@init_db); + command_noisy(@init_db); } setup_git_svn(); } @@ -334,70 +296,13 @@ sub init { sub fetch { check_upgrade_needed(); $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); - my $ret = $_use_lib ? fetch_lib(@_) : fetch_cmd(@_); - if ($ret->{commit} && quiet_run(qw(git-rev-parse --verify - refs/heads/master^0))) { - sys(qw(git-update-ref refs/heads/master),$ret->{commit}); + my $ret = fetch_lib(@_); + if ($ret->{commit} && !verify_ref('refs/heads/master^0')) { + command_noisy(qw(update-ref refs/heads/master),$ret->{commit}); } return $ret; } -sub fetch_cmd { - my (@parents) = @_; - my @log_args = -d $SVN_WC ? ($SVN_WC) : ($SVN_URL); - unless ($_revision) { - $_revision = -d $SVN_WC ? 'BASE:HEAD' : '0:HEAD'; - } - push @log_args, "-r$_revision"; - push @log_args, '--stop-on-copy' unless $_no_stop_copy; - - my $svn_log = svn_log_raw(@log_args); - - my $base = next_log_entry($svn_log) or croak "No base revision!\n"; - # don't need last_revision from grab_base_rev() because - # user could've specified a different revision to skip (they - # didn't want to import certain revisions into git for whatever - # reason, so trust $base->{revision} instead. - my (undef, $last_commit) = svn_grab_base_rev(); - unless (-d $SVN_WC) { - svn_cmd_checkout($SVN_URL,$base->{revision},$SVN_WC); - chdir $SVN_WC or croak $!; - read_uuid(); - $last_commit = git_commit($base, @parents); - assert_tree($last_commit); - } else { - chdir $SVN_WC or croak $!; - read_uuid(); - # looks like a user manually cp'd and svn switch'ed - unless ($last_commit) { - sys(qw/svn revert -R ./); - assert_svn_wc_clean($base->{revision}); - $last_commit = git_commit($base, @parents); - assert_tree($last_commit); - } - } - my @svn_up = qw(svn up); - push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - my $last = $base; - while (my $log_msg = next_log_entry($svn_log)) { - if ($last->{revision} >= $log_msg->{revision}) { - croak "Out of order: last >= current: ", - "$last->{revision} >= $log_msg->{revision}\n"; - } - # Revert is needed for cases like: - # https://svn.musicpd.org/Jamming/trunk (r166:167), but - # I can't seem to reproduce something like that on a test... - sys(qw/svn revert -R ./); - assert_svn_wc_clean($last->{revision}); - sys(@svn_up,"-r$log_msg->{revision}"); - $last_commit = git_commit($log_msg, $last_commit, @parents); - $last = $log_msg; - } - close $svn_log->{fh}; - $last->{commit} = $last_commit; - return $last; -} - sub fetch_lib { my (@parents) = @_; $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); @@ -416,16 +321,16 @@ sub fetch_lib { read_uuid(); if (defined $last_commit) { unless (-e $GIT_SVN_INDEX) { - sys(qw/git-read-tree/, $last_commit); + command_noisy('read-tree', $last_commit); } - chomp (my $x = `git-write-tree`); - my ($y) = (`git-cat-file commit $last_commit` + my $x = command_oneline('write-tree'); + my ($y) = (command(qw/cat-file commit/, $last_commit) =~ /^tree ($sha1)/m); if ($y ne $x) { unlink $GIT_SVN_INDEX or croak $!; - sys(qw/git-read-tree/, $last_commit); + command_noisy('read-tree', $last_commit); } - chomp ($x = `git-write-tree`); + $x = command_oneline('write-tree'); if ($y ne $x) { print STDERR "trees ($last_commit) $y != $x\n", "Something is seriously wrong...\n"; @@ -489,45 +394,19 @@ sub commit { } my @revs; foreach my $c (@commits) { - chomp(my @tmp = safe_qx('git-rev-parse',$c)); + my @tmp = command('rev-parse',$c); if (scalar @tmp == 1) { push @revs, $tmp[0]; } elsif (scalar @tmp > 1) { - push @revs, reverse (safe_qx('git-rev-list',@tmp)); + push @revs, reverse(command('rev-list',@tmp)); } else { die "Failed to rev-parse $c\n"; } } - chomp @revs; - $_use_lib ? commit_lib(@revs) : commit_cmd(@revs); + commit_lib(@revs); print "Done committing ",scalar @revs," revisions to SVN\n"; } -sub commit_cmd { - my (@revs) = @_; - - chdir $SVN_WC or croak "Unable to chdir $SVN_WC: $!\n"; - my $info = svn_info('.'); - my $fetched = fetch(); - if ($info->{Revision} != $fetched->{revision}) { - print STDERR "There are new revisions that were fetched ", - "and need to be merged (or acknowledged) ", - "before committing.\n"; - exit 1; - } - $info = svn_info('.'); - read_uuid($info); - my $last = $fetched; - foreach my $c (@revs) { - my $mods = svn_checkout_tree($last, $c); - if (scalar @$mods == 0) { - print "Skipping, no changes detected\n"; - next; - } - $last = svn_commit_tree($last, $c); - } -} - sub commit_lib { my (@revs) = @_; my ($r_last, $cmt_last) = svn_grab_base_rev(); @@ -606,10 +485,10 @@ sub commit_lib { sub dcommit { my $head = shift || 'HEAD'; my $gs = "refs/remotes/$GIT_SVN"; - chomp(my @refs = safe_qx(qw/git-rev-list --no-merges/, "$gs..$head")); + my @refs = command(qw/rev-list --no-merges/, "$gs..$head"); my $last_rev; foreach my $d (reverse @refs) { - if (quiet_run('git-rev-parse','--verify',"$d~1") != 0) { + if (!verify_ref("$d~1")) { die "Commit $d\n", "has no parent commit, and therefore ", "nothing to diff against.\n", @@ -633,7 +512,7 @@ sub dcommit { } return if $_dry_run; fetch(); - my @diff = safe_qx('git-diff-tree', $head, $gs); + my @diff = command('diff-tree', $head, $gs, '--'); my @finish; if (@diff) { @finish = qw/rebase/; @@ -645,37 +524,11 @@ sub dcommit { "Resetting to the latest $gs\n"; @finish = qw/reset --mixed/; } - sys('git', @finish, $gs); + command_noisy(@finish, $gs); } sub show_ignore { $SVN_URL ||= file_to_s("$GIT_SVN_DIR/info/url"); - $_use_lib ? show_ignore_lib() : show_ignore_cmd(); -} - -sub show_ignore_cmd { - require File::Find or die $!; - if (defined $_revision) { - die "-r/--revision option doesn't work unless the Perl SVN ", - "libraries are used\n"; - } - chdir $SVN_WC or croak $!; - my %ign; - File::Find::find({wanted=>sub{if(lstat $_ && -d _ && -d "$_/.svn"){ - s#^\./##; - @{$ign{$_}} = svn_propget_base('svn:ignore', $_); - }}, no_chdir=>1},'.'); - - print "\n# /\n"; - foreach (@{$ign{'.'}}) { print '/',$_ if /\S/ } - delete $ign{'.'}; - foreach my $i (sort keys %ign) { - print "\n# ",$i,"\n"; - foreach (@{$ign{$i}}) { print '/',$i,'/',$_ if /\S/ } - } -} - -sub show_ignore_lib { my $repo; $SVN ||= libsvn_connect($SVN_URL); my $r = defined $_revision ? $_revision : $SVN->get_latest_revnum; @@ -689,7 +542,7 @@ sub graft_branches { if (%$grafts) { # temporarily disable our grafts file to make this idempotent - chomp($gr_sha1 = safe_qx(qw/git-hash-object -w/,$gr_file)); + chomp($gr_sha1 = command(qw/hash-object -w/,$gr_file)); rename $gr_file, "$gr_file~$gr_sha1" or croak $!; } @@ -707,11 +560,7 @@ sub graft_branches { } } unless ($_no_graft_copy) { - if ($_use_lib) { - graft_file_copy_lib($grafts,$l_map,$u); - } else { - graft_file_copy_cmd($grafts,$l_map,$u); - } + graft_file_copy_lib($grafts,$l_map,$u); } } graft_tree_joins($grafts); @@ -743,7 +592,7 @@ sub multi_init { unless (-d $GIT_SVN_DIR) { print "GIT_SVN_ID set to 'trunk' for $_trunk\n" if $ch_id; init($_trunk); - sys('git-repo-config', 'svn.trunk', $_trunk); + command_noisy('repo-config', 'svn.trunk', $_trunk); } complete_url_ls_init($url, $_branches, '--branches/-b', ''); complete_url_ls_init($url, $_tags, '--tags/-t', 'tags/'); @@ -781,11 +630,8 @@ sub show_log { } config_pager(); - my $pid = open(my $log,'-|'); - defined $pid or croak $!; - if (!$pid) { - exec(git_svn_log_cmd($r_min,$r_max), @args) or croak $!; - } + @args = (git_svn_log_cmd($r_min, $r_max), @args); + my $log = command_output_pipe(@args); run_pager(); my (@k, $c, $d); @@ -832,7 +678,7 @@ sub show_log { process_commit($_, $r_min, $r_max) foreach reverse @k; } out: - close $log; + eval { command_close_pipe($log) }; print '-' x72,"\n" unless $_incremental || $_oneline; } @@ -842,10 +688,6 @@ sub commit_diff_usage { } sub commit_diff { - if (!$_use_lib) { - print STDERR "commit-diff must be used with SVN libraries\n"; - exit 1; - } my $ta = shift or commit_diff_usage(); my $tb = shift or commit_diff_usage(); if (!eval { $SVN_URL = shift || file_to_s("$GIT_SVN_DIR/info/url") }) { @@ -912,7 +754,7 @@ sub cmt_showable { return 1 if defined $c->{r}; if ($c->{l} && $c->{l}->[-1] eq "...\n" && $c->{a_raw} =~ /\@([a-f\d\-]+)>$/) { - my @msg = safe_qx(qw/git-cat-file commit/, $c->{c}); + my @msg = command(qw/cat-file commit/, $c->{c}); shift @msg while ($msg[0] ne "\n"); shift @msg; @{$c->{l}} = grep !/^git-svn-id: /, @msg; @@ -961,7 +803,7 @@ sub log_use_color { sub git_svn_log_cmd { my ($r_min, $r_max) = @_; - my @cmd = (qw/git-log --abbrev-commit --pretty=raw + my @cmd = (qw/log --abbrev-commit --pretty=raw --default/, "refs/remotes/$GIT_SVN"); push @cmd, '-r' unless $_non_recursive; push @cmd, qw/--raw --name-status/ if $_verbose; @@ -1046,8 +888,7 @@ sub complete_url_ls_init { } $var = $url . $var; } - chomp(my @ls = $_use_lib ? libsvn_ls_fullurl($var) - : safe_qx(qw/svn ls --non-interactive/, $var)); + my @ls = libsvn_ls_fullurl($var); my $old = $GIT_SVN; defined(my $pid = fork) or croak $!; if (!$pid) { @@ -1071,7 +912,7 @@ sub complete_url_ls_init { waitpid $pid, 0; croak $? if $?; my ($n) = ($switch =~ /^--(\w+)/); - sys('git-repo-config', "svn.$n", $var); + command_noisy('repo-config', "svn.$n", $var); } sub common_prefix { @@ -1103,11 +944,8 @@ sub graft_tree_joins { git_svn_each(sub { my $i = shift; - defined(my $pid = open my $fh, '-|') or croak $!; - if (!$pid) { - exec qw/git-rev-list --pretty=raw/, - "refs/remotes/$i" or croak $!; - } + my @args = (qw/rev-list --pretty=raw/, "refs/remotes/$i"); + my ($fh, $ctx) = command_output_pipe(@args); while (<$fh>) { next unless /^commit ($sha1)$/o; my $c = $1; @@ -1130,9 +968,7 @@ sub graft_tree_joins { foreach my $p (@{$tree_map{$t}}) { next if $p eq $c; - my $mb = eval { - safe_qx('git-merge-base', $c, $p) - }; + my $mb = eval { command('merge-base', $c, $p) }; next unless ($@ || $?); if (defined $r_a) { # see if SVN says it's a relative @@ -1161,41 +997,10 @@ sub graft_tree_joins { # what should we do when $ct == $s ? } } - close $fh or croak $?; + command_close_pipe($fh, $ctx); }); } -# this isn't funky-filename safe, but good enough for now... -sub graft_file_copy_cmd { - my ($grafts, $l_map, $u) = @_; - my $paths = $l_map->{$u}; - my $pfx = common_prefix([keys %$paths]); - $SVN_URL ||= $u.$pfx; - my $pid = open my $fh, '-|'; - defined $pid or croak $!; - unless ($pid) { - my @exec = qw/svn log -v/; - push @exec, "-r$_revision" if defined $_revision; - exec @exec, $u.$pfx or croak $!; - } - my ($r, $mp) = (undef, undef); - while (<$fh>) { - chomp; - if (/^\-{72}$/) { - $mp = $r = undef; - } elsif (/^r(\d+) \| /) { - $r = $1 unless defined $r; - } elsif (/^Changed paths:/) { - $mp = 1; - } elsif ($mp && m#^ [AR] /(\S.*?) \(from /(\S+?):(\d+)\)$#) { - my ($p1, $p0, $r0) = ($1, $2, $3); - my $c = find_graft_path_commit($paths, $p1, $r); - next unless $c; - find_graft_path_parents($grafts, $paths, $c, $p0, $r0); - } - } -} - sub graft_file_copy_lib { my ($grafts, $l_map, $u) = @_; my $tree_paths = $l_map->{$u}; @@ -1286,28 +1091,14 @@ sub graft_merge_msg { sub read_uuid { return if $SVN_UUID; - if ($_use_lib) { - my $pool = SVN::Pool->new; - $SVN_UUID = $SVN->get_uuid($pool); - $pool->clear; - } else { - my $info = shift || svn_info('.'); - $SVN_UUID = $info->{'Repository UUID'} or - croak "Repository UUID unreadable\n"; - } + my $pool = SVN::Pool->new; + $SVN_UUID = $SVN->get_uuid($pool); + $pool->clear; } -sub quiet_run { - my $pid = fork; - defined $pid or croak $!; - if (!$pid) { - open my $null, '>', '/dev/null' or croak $!; - open STDERR, '>&', $null or croak $!; - open STDOUT, '>&', $null or croak $!; - exec @_ or croak $!; - } - waitpid $pid, 0; - return $?; +sub verify_ref { + my ($ref) = @_; + eval { command_oneline([ 'rev-parse', $ref ], { STDERR => 0 }) }; } sub repo_path_split { @@ -1321,21 +1112,8 @@ sub repo_path_split { return ($u, $full_url); } } - if ($_use_lib) { - my $tmp = libsvn_connect($full_url); - return ($tmp->{repos_root}, $tmp->{svn_path}); - } else { - my ($url, $path) = ($full_url =~ m!^([a-z\+]+://[^/]*)(.*)$!i); - $path =~ s#^/+##; - my @paths = split(m#/+#, $path); - while (quiet_run(qw/svn ls --non-interactive/, $url)) { - my $n = shift @paths || last; - $url .= "/$n"; - } - push @repo_path_split_cache, qr/^(\Q$url\E)/; - $path = join('/',@paths); - return ($url, $path); - } + my $tmp = libsvn_connect($full_url); + return ($tmp->{repos_root}, $tmp->{svn_path}); } sub setup_git_svn { @@ -1351,41 +1129,17 @@ sub setup_git_svn { } -sub assert_svn_wc_clean { - return if $_use_lib; - my ($svn_rev) = @_; - croak "$svn_rev is not an integer!\n" unless ($svn_rev =~ /^\d+$/); - my $lcr = svn_info('.')->{'Last Changed Rev'}; - if ($svn_rev != $lcr) { - print STDERR "Checking for copy-tree ... "; - my @diff = grep(/^Index: /,(safe_qx(qw(svn diff), - "-r$lcr:$svn_rev"))); - if (@diff) { - croak "Nope! Expected r$svn_rev, got r$lcr\n"; - } else { - print STDERR "OK!\n"; - } - } - my @status = grep(!/^Performing status on external/,(`svn status`)); - @status = grep(!/^\s*$/,@status); - @status = grep(!/^X/,@status) if $_no_ignore_ext; - if (scalar @status) { - print STDERR "Tree ($SVN_WC) is not clean:\n"; - print STDERR $_ foreach @status; - croak; - } -} - sub get_tree_from_treeish { my ($treeish) = @_; croak "Not a sha1: $treeish\n" unless $treeish =~ /^$sha1$/o; - chomp(my $type = `git-cat-file -t $treeish`); + my $type = command_oneline(qw/cat-file -t/, $treeish); my $expected; while ($type eq 'tag') { - chomp(($treeish, $type) = `git-cat-file tag $treeish`); + ($treeish, $type) = command(qw/cat-file tag/, $treeish); } if ($type eq 'commit') { - $expected = (grep /^tree /,`git-cat-file commit $treeish`)[0]; + $expected = (grep /^tree /, command(qw/cat-file commit/, + $treeish))[0]; ($expected) = ($expected =~ /^tree ($sha1)$/); die "Unable to get tree from $treeish\n" unless $expected; } elsif ($type eq 'tree') { @@ -1396,27 +1150,19 @@ sub get_tree_from_treeish { return $expected; } -sub assert_tree { - return if $_use_lib; - my ($treeish) = @_; - my $expected = get_tree_from_treeish($treeish); - - my $tmpindex = $GIT_SVN_INDEX.'.assert-tmp'; - if (-e $tmpindex) { - unlink $tmpindex or croak $!; - } - my $old_index = set_index($tmpindex); - index_changes(1); - chomp(my $tree = `git-write-tree`); - restore_index($old_index); - if ($tree ne $expected) { - croak "Tree mismatch, Got: $tree, Expected: $expected\n"; +sub get_diff { + my ($from, $treeish) = @_; + print "diff-tree $from $treeish\n"; + my @diff_tree = qw(diff-tree -z -r); + if ($_cp_similarity) { + push @diff_tree, "-C$_cp_similarity"; + } else { + push @diff_tree, '-C'; } - unlink $tmpindex; -} - -sub parse_diff_tree { - my $diff_fh = shift; + push @diff_tree, '--find-copies-harder' if $_find_copies_harder; + push @diff_tree, "-l$_l" if defined $_l; + push @diff_tree, $from, $treeish; + my ($diff_fh, $ctx) = command_output_pipe(@diff_tree); local $/ = "\0"; my $state = 'meta'; my @mods; @@ -1452,170 +1198,10 @@ sub parse_diff_tree { croak "Error parsing $_\n"; } } - close $diff_fh or croak $?; - + command_close_pipe($diff_fh, $ctx); return \@mods; } -sub svn_check_prop_executable { - my $m = shift; - return if -l $m->{file_b}; - if ($m->{mode_b} =~ /755$/) { - chmod((0755 &~ umask),$m->{file_b}) or croak $!; - if ($m->{mode_a} !~ /755$/) { - sys(qw(svn propset svn:executable 1), $m->{file_b}); - } - -x $m->{file_b} or croak "$m->{file_b} is not executable!\n"; - } elsif ($m->{mode_b} !~ /755$/ && $m->{mode_a} =~ /755$/) { - sys(qw(svn propdel svn:executable), $m->{file_b}); - chmod((0644 &~ umask),$m->{file_b}) or croak $!; - -x $m->{file_b} and croak "$m->{file_b} is executable!\n"; - } -} - -sub svn_ensure_parent_path { - my $dir_b = dirname(shift); - svn_ensure_parent_path($dir_b) if ($dir_b ne File::Spec->curdir); - mkpath([$dir_b]) unless (-d $dir_b); - sys(qw(svn add -N), $dir_b) unless (-d "$dir_b/.svn"); -} - -sub precommit_check { - my $mods = shift; - my (%rm_file, %rmdir_check, %added_check); - - my %o = ( D => 0, R => 1, C => 2, A => 3, M => 3, T => 3 ); - foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) { - if ($m->{chg} eq 'R') { - if (-d $m->{file_b}) { - err_dir_to_file("$m->{file_a} => $m->{file_b}"); - } - # dir/$file => dir/file/$file - my $dirname = dirname($m->{file_b}); - while ($dirname ne File::Spec->curdir) { - if ($dirname ne $m->{file_a}) { - $dirname = dirname($dirname); - next; - } - err_file_to_dir("$m->{file_a} => $m->{file_b}"); - } - # baz/zzz => baz (baz is a file) - $dirname = dirname($m->{file_a}); - while ($dirname ne File::Spec->curdir) { - if ($dirname ne $m->{file_b}) { - $dirname = dirname($dirname); - next; - } - err_dir_to_file("$m->{file_a} => $m->{file_b}"); - } - } - if ($m->{chg} =~ /^(D|R)$/) { - my $t = $1 eq 'D' ? 'file_b' : 'file_a'; - $rm_file{ $m->{$t} } = 1; - my $dirname = dirname( $m->{$t} ); - my $basename = basename( $m->{$t} ); - $rmdir_check{$dirname}->{$basename} = 1; - } elsif ($m->{chg} =~ /^(?:A|C)$/) { - if (-d $m->{file_b}) { - err_dir_to_file($m->{file_b}); - } - my $dirname = dirname( $m->{file_b} ); - my $basename = basename( $m->{file_b} ); - $added_check{$dirname}->{$basename} = 1; - while ($dirname ne File::Spec->curdir) { - if ($rm_file{$dirname}) { - err_file_to_dir($m->{file_b}); - } - $dirname = dirname $dirname; - } - } - } - return (\%rmdir_check, \%added_check); - - sub err_dir_to_file { - my $file = shift; - print STDERR "Node change from directory to file ", - "is not supported by Subversion: ",$file,"\n"; - exit 1; - } - sub err_file_to_dir { - my $file = shift; - print STDERR "Node change from file to directory ", - "is not supported by Subversion: ",$file,"\n"; - exit 1; - } -} - - -sub get_diff { - my ($from, $treeish) = @_; - assert_tree($from); - print "diff-tree $from $treeish\n"; - my $pid = open my $diff_fh, '-|'; - defined $pid or croak $!; - if ($pid == 0) { - my @diff_tree = qw(git-diff-tree -z -r); - if ($_cp_similarity) { - push @diff_tree, "-C$_cp_similarity"; - } else { - push @diff_tree, '-C'; - } - push @diff_tree, '--find-copies-harder' if $_find_copies_harder; - push @diff_tree, "-l$_l" if defined $_l; - exec(@diff_tree, $from, $treeish) or croak $!; - } - return parse_diff_tree($diff_fh); -} - -sub svn_checkout_tree { - my ($from, $treeish) = @_; - my $mods = get_diff($from->{commit}, $treeish); - return $mods unless (scalar @$mods); - my ($rm, $add) = precommit_check($mods); - - my %o = ( D => 1, R => 0, C => -1, A => 3, M => 3, T => 3 ); - foreach my $m (sort { $o{$a->{chg}} <=> $o{$b->{chg}} } @$mods) { - if ($m->{chg} eq 'C') { - svn_ensure_parent_path( $m->{file_b} ); - sys(qw(svn cp), $m->{file_a}, $m->{file_b}); - apply_mod_line_blob($m); - svn_check_prop_executable($m); - } elsif ($m->{chg} eq 'D') { - sys(qw(svn rm --force), $m->{file_b}); - } elsif ($m->{chg} eq 'R') { - svn_ensure_parent_path( $m->{file_b} ); - sys(qw(svn mv --force), $m->{file_a}, $m->{file_b}); - apply_mod_line_blob($m); - svn_check_prop_executable($m); - } elsif ($m->{chg} eq 'M') { - apply_mod_line_blob($m); - svn_check_prop_executable($m); - } elsif ($m->{chg} eq 'T') { - svn_check_prop_executable($m); - apply_mod_line_blob($m); - if ($m->{mode_a} =~ /^120/ && $m->{mode_b} !~ /^120/) { - sys(qw(svn propdel svn:special), $m->{file_b}); - } else { - sys(qw(svn propset svn:special *),$m->{file_b}); - } - } elsif ($m->{chg} eq 'A') { - svn_ensure_parent_path( $m->{file_b} ); - apply_mod_line_blob($m); - sys(qw(svn add), $m->{file_b}); - svn_check_prop_executable($m); - } else { - croak "Invalid chg: $m->{chg}\n"; - } - } - - assert_tree($treeish); - if ($_rmdir) { # remove empty directories - handle_rmdir($rm, $add); - } - assert_tree($treeish); - return $mods; -} - sub libsvn_checkout_tree { my ($from, $treeish, $ed) = @_; my $mods = get_diff($from, $treeish); @@ -1633,57 +1219,15 @@ sub libsvn_checkout_tree { return $mods; } -# svn ls doesn't work with respect to the current working tree, but what's -# in the repository. There's not even an option for it... *sigh* -# (added files don't show up and removed files remain in the ls listing) -sub svn_ls_current { - my ($dir, $rm, $add) = @_; - chomp(my @ls = safe_qx('svn','ls',$dir)); - my @ret = (); - foreach (@ls) { - s#/$##; # trailing slashes are evil - push @ret, $_ unless $rm->{$dir}->{$_}; - } - if (exists $add->{$dir}) { - push @ret, keys %{$add->{$dir}}; - } - return \@ret; -} - -sub handle_rmdir { - my ($rm, $add) = @_; - - foreach my $dir (sort {length $b <=> length $a} keys %$rm) { - my $ls = svn_ls_current($dir, $rm, $add); - next if (scalar @$ls); - sys(qw(svn rm --force),$dir); - - my $dn = dirname $dir; - $rm->{ $dn }->{ basename $dir } = 1; - $ls = svn_ls_current($dn, $rm, $add); - while (scalar @$ls == 0 && $dn ne File::Spec->curdir) { - sys(qw(svn rm --force),$dn); - $dir = basename $dn; - $dn = dirname $dn; - $rm->{ $dn }->{ $dir } = 1; - $ls = svn_ls_current($dn, $rm, $add); - } - } -} - sub get_commit_message { my ($commit, $commit_msg) = (@_); my %log_msg = ( msg => '' ); open my $msg, '>', $commit_msg or croak $!; - chomp(my $type = `git-cat-file -t $commit`); + my $type = command_oneline(qw/cat-file -t/, $commit); if ($type eq 'commit' || $type eq 'tag') { - my $pid = open my $msg_fh, '-|'; - defined $pid or croak $!; - - if ($pid == 0) { - exec('git-cat-file', $type, $commit) or croak $!; - } + my ($msg_fh, $ctx) = command_output_pipe('cat-file', + $type, $commit); my $in_msg = 0; while (<$msg_fh>) { if (!$in_msg) { @@ -1695,7 +1239,7 @@ sub get_commit_message { print $msg $_ or croak $!; } } - close $msg_fh or croak $?; + command_close_pipe($msg_fh, $ctx); } close $msg or croak $!; @@ -1720,64 +1264,9 @@ sub set_svn_commit_env { } } -sub svn_commit_tree { - my ($last, $commit) = @_; - my $commit_msg = "$GIT_SVN_DIR/.svn-commit.tmp.$$"; - my $log_msg = get_commit_message($commit, $commit_msg); - my ($oneline) = ($log_msg->{msg} =~ /([^\n\r]+)/); - print "Committing $commit: $oneline\n"; - - set_svn_commit_env(); - my @ci_output = safe_qx(qw(svn commit -F),$commit_msg); - $ENV{LC_ALL} = 'C'; - unlink $commit_msg; - my ($committed) = ($ci_output[$#ci_output] =~ /(\d+)/); - if (!defined $committed) { - my $out = join("\n",@ci_output); - print STDERR "W: Trouble parsing \`svn commit' output:\n\n", - $out, "\n\nAssuming English locale..."; - ($committed) = ($out =~ /^Committed revision \d+\./sm); - defined $committed or die " FAILED!\n", - "Commit output failed to parse committed revision!\n", - print STDERR " OK\n"; - } - - my @svn_up = qw(svn up); - push @svn_up, '--ignore-externals' unless $_no_ignore_ext; - if ($_optimize_commits && ($committed == ($last->{revision} + 1))) { - push @svn_up, "-r$committed"; - sys(@svn_up); - my $info = svn_info('.'); - my $date = $info->{'Last Changed Date'} or die "Missing date\n"; - if ($info->{'Last Changed Rev'} != $committed) { - croak "$info->{'Last Changed Rev'} != $committed\n" - } - my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~ - /(\d{4})\-(\d\d)\-(\d\d)\s - (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x) - or croak "Failed to parse date: $date\n"; - $log_msg->{date} = "$tz $Y-$m-$d $H:$M:$S"; - $log_msg->{author} = $info->{'Last Changed Author'}; - $log_msg->{revision} = $committed; - $log_msg->{msg} .= "\n"; - $log_msg->{parents} = [ $last->{commit} ]; - $log_msg->{commit} = git_commit($log_msg, $commit); - return $log_msg; - } - # resync immediately - push @svn_up, "-r$last->{revision}"; - sys(@svn_up); - return fetch("$committed=$commit"); -} - sub rev_list_raw { - my (@args) = @_; - my $pid = open my $fh, '-|'; - defined $pid or croak $!; - if (!$pid) { - exec(qw/git-rev-list --pretty=raw/, @args) or croak $!; - } - return { fh => $fh, t => { } }; + my ($fh, $c) = command_output_pipe(qw/rev-list --pretty=raw/, @_); + return { fh => $fh, ctx => $c, t => { } }; } sub next_rev_list_entry { @@ -1799,184 +1288,10 @@ sub next_rev_list_entry { $x->{m} .= $_; } } + command_close_pipe($fh, $rl->{ctx}); return ($x != $rl->{t}) ? $x : undef; } -# read the entire log into a temporary file (which is removed ASAP) -# and store the file handle + parser state -sub svn_log_raw { - my (@log_args) = @_; - my $log_fh = IO::File->new_tmpfile or croak $!; - my $pid = fork; - defined $pid or croak $!; - if (!$pid) { - open STDOUT, '>&', $log_fh or croak $!; - exec (qw(svn log), @log_args) or croak $! - } - waitpid $pid, 0; - croak $? if $?; - seek $log_fh, 0, 0 or croak $!; - return { state => 'sep', fh => $log_fh }; -} - -sub next_log_entry { - my $log = shift; # retval of svn_log_raw() - my $ret = undef; - my $fh = $log->{fh}; - - while (<$fh>) { - chomp; - if (/^\-{72}$/) { - if ($log->{state} eq 'msg') { - if ($ret->{lines}) { - $ret->{msg} .= $_."\n"; - unless(--$ret->{lines}) { - $log->{state} = 'sep'; - } - } else { - croak "Log parse error at: $_\n", - $ret->{revision}, - "\n"; - } - next; - } - if ($log->{state} ne 'sep') { - croak "Log parse error at: $_\n", - "state: $log->{state}\n", - $ret->{revision}, - "\n"; - } - $log->{state} = 'rev'; - - # if we have an empty log message, put something there: - if ($ret) { - $ret->{msg} ||= "\n"; - delete $ret->{lines}; - return $ret; - } - next; - } - if ($log->{state} eq 'rev' && s/^r(\d+)\s*\|\s*//) { - my $rev = $1; - my ($author, $date, $lines) = split(/\s*\|\s*/, $_, 3); - ($lines) = ($lines =~ /(\d+)/); - $date = '1970-01-01 00:00:00 +0000' - if ($_ignore_nodate && $date eq '(no date)'); - my ($Y,$m,$d,$H,$M,$S,$tz) = ($date =~ - /(\d{4})\-(\d\d)\-(\d\d)\s - (\d\d)\:(\d\d)\:(\d\d)\s([\-\+]\d+)/x) - or croak "Failed to parse date: $date\n"; - $ret = { revision => $rev, - date => "$tz $Y-$m-$d $H:$M:$S", - author => $author, - lines => $lines, - msg => '' }; - if (defined $_authors && ! defined $users{$author}) { - die "Author: $author not defined in ", - "$_authors file\n"; - } - $log->{state} = 'msg_start'; - next; - } - # skip the first blank line of the message: - if ($log->{state} eq 'msg_start' && /^$/) { - $log->{state} = 'msg'; - } elsif ($log->{state} eq 'msg') { - if ($ret->{lines}) { - $ret->{msg} .= $_."\n"; - unless (--$ret->{lines}) { - $log->{state} = 'sep'; - } - } else { - croak "Log parse error at: $_\n", - $ret->{revision},"\n"; - } - } - } - return $ret; -} - -sub svn_info { - my $url = shift || $SVN_URL; - - my $pid = open my $info_fh, '-|'; - defined $pid or croak $!; - - if ($pid == 0) { - exec(qw(svn info),$url) or croak $!; - } - - my $ret = {}; - # only single-lines seem to exist in svn info output - while (<$info_fh>) { - chomp $_; - if (m#^([^:]+)\s*:\s*(\S.*)$#) { - $ret->{$1} = $2; - push @{$ret->{-order}}, $1; - } - } - close $info_fh or croak $?; - return $ret; -} - -sub sys { system(@_) == 0 or croak $? } - -sub do_update_index { - my ($z_cmd, $cmd, $no_text_base) = @_; - - my $z = open my $p, '-|'; - defined $z or croak $!; - unless ($z) { exec @$z_cmd or croak $! } - - my $pid = open my $ui, '|-'; - defined $pid or croak $!; - unless ($pid) { - exec('git-update-index',"--$cmd",'-z','--stdin') or croak $!; - } - local $/ = "\0"; - while (my $x = <$p>) { - chomp $x; - if (!$no_text_base && lstat $x && ! -l _ && - svn_propget_base('svn:keywords', $x)) { - my $mode = -x _ ? 0755 : 0644; - my ($v,$d,$f) = File::Spec->splitpath($x); - my $tb = File::Spec->catfile($d, '.svn', 'tmp', - 'text-base',"$f.svn-base"); - $tb =~ s#^/##; - unless (-f $tb) { - $tb = File::Spec->catfile($d, '.svn', - 'text-base',"$f.svn-base"); - $tb =~ s#^/##; - } - my @s = stat($x); - unlink $x or croak $!; - copy($tb, $x); - chmod(($mode &~ umask), $x) or croak $!; - utime $s[8], $s[9], $x; - } - print $ui $x,"\0"; - } - close $ui or croak $?; -} - -sub index_changes { - return if $_use_lib; - - if (!-f "$GIT_SVN_DIR/info/exclude") { - open my $fd, '>>', "$GIT_SVN_DIR/info/exclude" or croak $!; - print $fd '.svn',"\n"; - close $fd or croak $!; - } - my $no_text_base = shift; - do_update_index([qw/git-diff-files --name-only -z/], - 'remove', - $no_text_base); - do_update_index([qw/git-ls-files -z --others/, - "--exclude-from=$GIT_SVN_DIR/info/exclude"], - 'add', - $no_text_base); -} - sub s_to_file { my ($str, $file, $mode) = @_; open my $fd,'>',$file or croak $!; @@ -2002,18 +1317,6 @@ sub assert_revision_unknown { } } -sub trees_eq { - my ($x, $y) = @_; - my @x = safe_qx('git-cat-file','commit',$x); - my @y = safe_qx('git-cat-file','commit',$y); - if (($y[0] ne $x[0]) || $x[0] !~ /^tree $sha1\n$/ - || $y[0] !~ /^tree $sha1\n$/) { - print STDERR "Trees not equal: $y[0] != $x[0]\n"; - return 0 - } - return 1; -} - sub git_commit { my ($log_msg, @parents) = @_; assert_revision_unknown($log_msg->{revision}); @@ -2038,34 +1341,23 @@ sub git_commit { my $tree = $log_msg->{tree}; if (!defined $tree) { my $index = set_index($GIT_SVN_INDEX); - index_changes(); - chomp($tree = `git-write-tree`); + $tree = command_oneline('write-tree'); croak $? if $?; restore_index($index); } - # just in case we clobber the existing ref, we still want that ref # as our parent: - open my $null, '>', '/dev/null' or croak $!; - open my $stderr, '>&', \*STDERR or croak $!; - open STDERR, '>&', $null or croak $!; - if (my $cur = eval { safe_qx('git-rev-parse', - "refs/remotes/$GIT_SVN^0") }) { + if (my $cur = verify_ref("refs/remotes/$GIT_SVN^0")) { chomp $cur; push @tmp_parents, $cur; } - open STDERR, '>&', $stderr or croak $!; - close $stderr or croak $!; - close $null or croak $!; if (exists $tree_map{$tree}) { foreach my $p (@{$tree_map{$tree}}) { my $skip; foreach (@tmp_parents) { # see if a common parent is found - my $mb = eval { - safe_qx('git-merge-base', $_, $p) - }; + my $mb = eval { command('merge-base', $_, $p) }; next if ($@ || $?); $skip = 1; last; @@ -2107,7 +1399,7 @@ sub git_commit { if ($commit !~ /^$sha1$/o) { die "Failed to commit, invalid sha1: $commit\n"; } - sys('git-update-ref',"refs/remotes/$GIT_SVN",$commit); + command_noisy('update-ref',"refs/remotes/$GIT_SVN",$commit); revdb_set($REVDB, $log_msg->{revision}, $commit); # this output is read via pipe, do not change: @@ -2119,7 +1411,8 @@ sub git_commit { sub check_repack { if ($_repack && (--$_repack_nr == 0)) { $_repack_nr = $_repack; - sys("git repack $_repack_flags"); + # repack doesn't use any arguments with spaces in them, does it? + command_noisy('repack', split(/\s+/, $_repack_flags)); } } @@ -2136,122 +1429,17 @@ sub set_commit_env { $ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $log_msg->{date}; } -sub apply_mod_line_blob { - my $m = shift; - if ($m->{mode_b} =~ /^120/) { - blob_to_symlink($m->{sha1_b}, $m->{file_b}); - } else { - blob_to_file($m->{sha1_b}, $m->{file_b}); - } -} - -sub blob_to_symlink { - my ($blob, $link) = @_; - defined $link or croak "\$link not defined!\n"; - croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o; - if (-l $link || -f _) { - unlink $link or croak $!; - } - - my $dest = `git-cat-file blob $blob`; # no newline, so no chomp - symlink $dest, $link or croak $!; -} - -sub blob_to_file { - my ($blob, $file) = @_; - defined $file or croak "\$file not defined!\n"; - croak "Not a sha1: $blob\n" unless $blob =~ /^$sha1$/o; - if (-l $file || -f _) { - unlink $file or croak $!; - } - - open my $blob_fh, '>', $file or croak "$!: $file\n"; - my $pid = fork; - defined $pid or croak $!; - - if ($pid == 0) { - open STDOUT, '>&', $blob_fh or croak $!; - exec('git-cat-file','blob',$blob) or croak $!; - } - waitpid $pid, 0; - croak $? if $?; - - close $blob_fh or croak $!; -} - -sub safe_qx { - my $pid = open my $child, '-|'; - defined $pid or croak $!; - if ($pid == 0) { - exec(@_) or croak $!; - } - my @ret = (<$child>); - close $child or croak $?; - die $? if $?; # just in case close didn't error out - return wantarray ? @ret : join('',@ret); -} - -sub svn_compat_check { - if ($_follow_parent) { - print STDERR 'E: --follow-parent functionality is only ', - "available when SVN libraries are used\n"; - exit 1; - } - my @co_help = safe_qx(qw(svn co -h)); - unless (grep /ignore-externals/,@co_help) { - print STDERR "W: Installed svn version does not support ", - "--ignore-externals\n"; - $_no_ignore_ext = 1; - } - if (grep /usage: checkout URL\[\@REV\]/,@co_help) { - $_svn_co_url_revs = 1; - } - if (grep /\[TARGET\[\@REV\]\.\.\.\]/, `svn propget -h`) { - $_svn_pg_peg_revs = 1; - } - - # I really, really hope nobody hits this... - unless (grep /stop-on-copy/, (safe_qx(qw(svn log -h)))) { - print STDERR <<''; -W: The installed svn version does not support the --stop-on-copy flag in - the log command. - Lets hope the directory you're tracking is not a branch or tag - and was never moved within the repository... - - $_no_stop_copy = 1; - } -} - -# *sigh*, new versions of svn won't honor -r<rev> without URL@<rev>, -# (and they won't honor URL@<rev> without -r<rev>, too!) -sub svn_cmd_checkout { - my ($url, $rev, $dir) = @_; - my @cmd = ('svn','co', "-r$rev"); - push @cmd, '--ignore-externals' unless $_no_ignore_ext; - $url .= "\@$rev" if $_svn_co_url_revs; - sys(@cmd, $url, $dir); -} - sub check_upgrade_needed { if (!-r $REVDB) { -d $GIT_SVN_DIR or mkpath([$GIT_SVN_DIR]); open my $fh, '>>',$REVDB or croak $!; close $fh; } - my $old = eval { - my $pid = open my $child, '-|'; - defined $pid or croak $!; - if ($pid == 0) { - close STDERR; - exec('git-rev-parse',"$GIT_SVN-HEAD") or croak $!; - } - my @ret = (<$child>); - close $child or croak $?; - die $? if $?; # just in case close didn't error out - return wantarray ? @ret : join('',@ret); + return unless eval { + command([qw/rev-parse --verify/,"$GIT_SVN-HEAD^0"], + {STDERR => 0}); }; - return unless $old; - my $head = eval { safe_qx('git-rev-parse',"refs/remotes/$GIT_SVN") }; + my $head = eval { command('rev-parse',"refs/remotes/$GIT_SVN") }; if ($@ || !$head) { print STDERR "Please run: $0 rebuild --upgrade\n"; exit 1; @@ -2263,12 +1451,8 @@ sub check_upgrade_needed { sub map_tree_joins { my %seen; foreach my $br (@_branch_from) { - my $pid = open my $pipe, '-|'; - defined $pid or croak $!; - if ($pid == 0) { - exec(qw(git-rev-list --topo-order --pretty=raw), $br) - or croak $!; - } + my $pipe = command_output_pipe(qw/rev-list + --topo-order --pretty=raw/, $br); while (<$pipe>) { if (/^commit ($sha1)$/o) { my $commit = $1; @@ -2284,7 +1468,7 @@ sub map_tree_joins { $seen{$commit} = 1; } } - close $pipe; # we could be breaking the pipe early + eval { command_close_pipe($pipe) }; } } @@ -2296,7 +1480,7 @@ sub load_all_refs { # don't worry about rev-list on non-commit objects/tags, # it shouldn't blow up if a ref is a blob or tree... - chomp(@_branch_from = `git-rev-parse --symbolic --all`); + @_branch_from = command(qw/rev-parse --symbolic --all/); } # '<svn username> = real-name <email address>' mapping based on git-svnimport: @@ -2322,15 +1506,9 @@ sub rload_authors { close $authors or croak $!; } -sub svn_propget_base { - my ($p, $f) = @_; - $f .= '@BASE' if $_svn_pg_peg_revs; - return safe_qx(qw/svn propget/, $p, $f); -} - sub git_svn_each { my $sub = shift; - foreach (`git-rev-parse --symbolic --all`) { + foreach (command(qw/rev-parse --symbolic --all/)) { next unless s#^refs/remotes/##; chomp $_; next unless -f "$GIT_DIR/svn/$_/info/url"; @@ -2371,7 +1549,7 @@ sub migration_check { "$GIT_SVN_DIR\n\t(required for this version ", "($VERSION) of git-svn) does not.\n"; - foreach my $x (`git-rev-parse --symbolic --all`) { + foreach my $x (command(qw/rev-parse --symbolic --all/)) { next unless $x =~ s#^refs/remotes/##; chomp $x; next unless -f "$GIT_DIR/$x/info/url"; @@ -2476,11 +1654,7 @@ sub write_grafts { my $p = $grafts->{$c}; my %x; # real parents delete $p->{$c}; # commits are not self-reproducing... - my $pid = open my $ch, '-|'; - defined $pid or croak $!; - if (!$pid) { - exec(qw/git-cat-file commit/, $c) or croak $!; - } + my $ch = command_output_pipe(qw/cat-file commit/, $c); while (<$ch>) { if (/^parent ($sha1)/) { $x{$1} = $p->{$1} = 1; @@ -2488,7 +1662,7 @@ sub write_grafts { last unless /^\S/; } } - close $ch; # breaking the pipe + eval { command_close_pipe($ch) }; # breaking the pipe # if real parents are the only ones in the grafts, drop it next if join(' ',sort keys %$p) eq join(' ',sort keys %x); @@ -2500,7 +1674,7 @@ sub write_grafts { next if $del{$i} || $p->{$i} == 2; foreach my $j (@jp) { next if $i eq $j || $del{$j} || $p->{$j} == 2; - $mb = eval { safe_qx('git-merge-base',$i,$j) }; + $mb = eval { command('merge-base', $i, $j) }; next unless $mb; chomp $mb; next if $x{$mb}; @@ -2571,15 +1745,12 @@ sub extract_metadata { sub cmt_metadata { return extract_metadata((grep(/^git-svn-id: /, - safe_qx(qw/git-cat-file commit/, shift)))[-1]); + command(qw/cat-file commit/, shift)))[-1]); } sub get_commit_time { my $cmt = shift; - defined(my $pid = open my $fh, '-|') or croak $!; - if (!$pid) { - exec qw/git-rev-list --pretty=raw -n1/, $cmt or croak $!; - } + my $fh = command_output_pipe(qw/rev-list --pretty=raw -n1/, $cmt); while (<$fh>) { /^committer\s(?:.+) (\d+) ([\-\+]?\d+)$/ or next; my ($s, $tz) = ($1, $2); @@ -2588,7 +1759,7 @@ sub get_commit_time { } elsif ($tz =~ s/^\-//) { $s -= tz_to_s_offset($tz); } - close $fh; + eval { command_close_pipe($fh) }; return $s; } die "Can't get commit time for commit: $cmt\n"; @@ -2734,34 +1905,6 @@ sub show_commit_normal { } } -sub libsvn_load { - return unless $_use_lib; - $_use_lib = eval { - require SVN::Core; - if ($SVN::Core::VERSION lt '1.1.0') { - die "Need SVN::Core 1.1.0 or better ", - "(got $SVN::Core::VERSION) ", - "Falling back to command-line svn\n"; - } - require SVN::Ra; - require SVN::Delta; - push @SVN::Git::Editor::ISA, 'SVN::Delta::Editor'; - push @SVN::Git::Fetcher::ISA, 'SVN::Delta::Editor'; - *SVN::Git::Fetcher::process_rm = *process_rm; - *SVN::Git::Fetcher::safe_qx = *safe_qx; - my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file. - $SVN::Node::dir.$SVN::Node::unknown. - $SVN::Node::none.$SVN::Node::file. - $SVN::Node::dir.$SVN::Node::unknown. - $SVN::Auth::SSL::CNMISMATCH. - $SVN::Auth::SSL::NOTYETVALID. - $SVN::Auth::SSL::EXPIRED. - $SVN::Auth::SSL::UNKNOWNCA. - $SVN::Auth::SSL::OTHER; - 1; - }; -} - sub _simple_prompt { my ($cred, $realm, $default_username, $may_save, $pool) = @_; $may_save = undef if $_no_auth_cache; @@ -2965,7 +2108,7 @@ sub libsvn_get_file { my $mode = exists $props->{'svn:executable'} ? '100755' : '100644'; if (exists $props->{'svn:special'}) { $mode = '120000'; - my $link = `git-cat-file blob $hash`; + my $link = `git-cat-file blob $hash`; # no chomping symlinks $link =~ s/^link // or die "svn:special file with contents: <", $link, "> is not understood\n"; defined($pid = open3($in, $out, '>&STDERR', @@ -3069,19 +2212,17 @@ sub libsvn_log_entry { sub process_rm { my ($gui, $last_commit, $f, $q) = @_; # remove entire directories. - if (safe_qx('git-ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) { - defined(my $pid = open my $ls, '-|') or croak $!; - if (!$pid) { - exec(qw/git-ls-tree -r --name-only -z/, - $last_commit,'--',$f) or croak $!; - } + if (command('ls-tree',$last_commit,'--',$f) =~ /^040000 tree/) { + my ($ls, $ctx) = command_output_pipe(qw/ls-tree + -r --name-only -z/, + $last_commit,'--',$f); local $/ = "\0"; while (<$ls>) { print $gui '0 ',0 x 40,"\t",$_ or croak $!; print "\tD\t$_\n" unless $q; } print "\tD\t$f/\n" unless $q; - close $ls or croak $?; + command_close_pipe($ls, $ctx); return $SVN::Node::dir; } else { print $gui '0 ',0 x 40,"\t",$f,"\0" or croak $!; @@ -3112,7 +2253,7 @@ sub libsvn_fetch_delta { sub libsvn_fetch_full { my ($last_commit, $paths, $rev, $author, $date, $msg) = @_; - open my $gui, '| git-update-index -z --index-info' or croak $!; + my ($gui, $ctx) = command_input_pipe(qw/update-index -z --index-info/); my %amr; my $ut = { empty => {}, dir_prop => {}, file_prop => {} }; my $p = $SVN->{svn_path}; @@ -3166,20 +2307,14 @@ sub libsvn_fetch_full { %{$ut->{dir_prop}->{''}} = %$props; $pool->clear; } - close $gui or croak $?; + command_close_pipe($gui, $ctx); libsvn_log_entry($rev, $author, $date, $msg, [$last_commit], $ut); } sub svn_grab_base_rev { - defined(my $pid = open my $fh, '-|') or croak $!; - if (!$pid) { - open my $null, '>', '/dev/null' or croak $!; - open STDERR, '>&', $null or croak $!; - exec qw/git-rev-parse --verify/,"refs/remotes/$GIT_SVN^0" - or croak $!; - } - chomp(my $c = do { local $/; <$fh> }); - close $fh; + my $c = eval { command_oneline([qw/rev-parse --verify/, + "refs/remotes/$GIT_SVN^0"], + { STDERR => 0 }) }; if (defined $c && length $c) { my ($url, $rev, $uuid) = cmt_metadata($c); return ($rev, $c) if defined $rev; @@ -3292,18 +2427,11 @@ sub revisions_eq { my ($path, $r0, $r1) = @_; return 1 if $r0 == $r1; my $nr = 0; - if ($_use_lib) { - # should be OK to use Pool here (r1 - r0) should be small - my $pool = SVN::Pool->new; - libsvn_get_log($SVN, [$path], $r0, $r1, - 0, 0, 1, sub {$nr++}, $pool); - $pool->clear; - } else { - my ($url, undef) = repo_path_split($SVN_URL); - my $svn_log = svn_log_raw("$url/$path","-r$r0:$r1"); - while (next_log_entry($svn_log)) { $nr++ } - close $svn_log->{fh}; - } + # should be OK to use Pool here (r1 - r0) should be small + my $pool = SVN::Pool->new; + libsvn_get_log($SVN, [$path], $r0, $r1, + 0, 0, 1, sub {$nr++}, $pool); + $pool->clear; return 0 if ($nr > 1); return 1; } @@ -3358,7 +2486,7 @@ sub libsvn_find_parent_branch { if (revisions_eq($branch_from, $r0, $r)) { unlink $GIT_SVN_INDEX; print STDERR "Found branch parent: ($GIT_SVN) $parent\n"; - sys(qw/git-read-tree/, $parent); + command_noisy('read-tree', $parent); unless (libsvn_can_do_switch()) { return libsvn_fetch_full($parent, $paths, $rev, $author, $date, $msg); @@ -3367,7 +2495,7 @@ sub libsvn_find_parent_branch { # included with SVN 1.4.2 (the latest version at the moment), # so we can't rely on it. my $ra = libsvn_connect("$url/$branch_from"); - my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q}); + my $ed = SVN::Git::Fetcher->new({c => $parent, q => $_q }); my $pool = SVN::Pool->new; my $reporter = $ra->do_switch($rev, '', 1, $SVN->{url}, $ed, $pool); @@ -3413,9 +2541,10 @@ sub libsvn_new_tree { $ut = $ed; } else { $ut = { empty => {}, dir_prop => {}, file_prop => {} }; - open my $gui, '| git-update-index -z --index-info' or croak $!; + my ($gui, $ctx) = command_input_pipe(qw/update-index + -z --index-info/); libsvn_traverse($gui, '', $SVN->{svn_path}, $rev, undef, $ut); - close $gui or croak $?; + command_close_pipe($gui, $ctx); } libsvn_log_entry($rev, $author, $date, $msg, [], $ut); } @@ -3487,7 +2616,7 @@ sub libsvn_commit_cb { my $log = libsvn_log_entry($rev,$committer,$date,$msg); $log->{tree} = get_tree_from_treeish($c); my $cmt = git_commit($log, $cmt_last, $c); - my @diff = safe_qx('git-diff-tree', $cmt, $c); + my @diff = command('diff-tree', $cmt, $c); if (@diff) { print STDERR "Trees differ: $cmt $c\n", join('',@diff),"\n"; @@ -3579,27 +2708,40 @@ sub revdb_get { sub copy_remote_ref { my $origin = $_cp_remote ? $_cp_remote : 'origin'; my $ref = "refs/remotes/$GIT_SVN"; - if (safe_qx('git-ls-remote', $origin, $ref)) { - sys(qw/git fetch/, $origin, "$ref:$ref"); + if (command('ls-remote', $origin, $ref)) { + command_noisy('fetch', $origin, "$ref:$ref"); } elsif ($_cp_remote && !$_upgrade) { die "Unable to find remote reference: ", "refs/remotes/$GIT_SVN on $origin\n"; } } + +{ + my $kill_stupid_warnings = $SVN::Node::none.$SVN::Node::file. + $SVN::Node::dir.$SVN::Node::unknown. + $SVN::Node::none.$SVN::Node::file. + $SVN::Node::dir.$SVN::Node::unknown. + $SVN::Auth::SSL::CNMISMATCH. + $SVN::Auth::SSL::NOTYETVALID. + $SVN::Auth::SSL::EXPIRED. + $SVN::Auth::SSL::UNKNOWNCA. + $SVN::Auth::SSL::OTHER; +} + package SVN::Git::Fetcher; use vars qw/@ISA/; use strict; use warnings; use Carp qw/croak/; use IO::File qw//; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; # file baton members: path, mode_a, mode_b, pool, fh, blob, base sub new { my ($class, $git_svn) = @_; my $self = SVN::Delta::Editor->new; bless $self, $class; - open my $gui, '| git-update-index -z --index-info' or croak $!; - $self->{gui} = $gui; $self->{c} = $git_svn->{c} if exists $git_svn->{c}; $self->{q} = $git_svn->{q}; $self->{empty} = {}; @@ -3607,6 +2749,8 @@ sub new { $self->{file_prop} = {}; $self->{absent_dir} = {}; $self->{absent_file} = {}; + ($self->{gui}, $self->{ctx}) = command_input_pipe( + qw/update-index -z --index-info/); require Digest::MD5; $self; } @@ -3629,7 +2773,7 @@ sub delete_entry { sub open_file { my ($self, $path, $pb, $rev) = @_; - my ($mode, $blob) = (safe_qx('git-ls-tree',$self->{c},'--',$path) + my ($mode, $blob) = (command('ls-tree', $self->{c}, '--',$path) =~ /^(\d{6}) blob ([a-f\d]{40})\t/); unless (defined $mode && defined $blob) { die "$path was not found in commit $self->{c} (r$rev)\n"; @@ -3764,13 +2908,13 @@ sub close_file { sub abort_edit { my $self = shift; - close $self->{gui}; + eval { command_close_pipe($self->{gui}, $self->{ctx}) }; $self->SUPER::abort_edit(@_); } sub close_edit { my $self = shift; - close $self->{gui} or croak $!; + command_close_pipe($self->{gui}, $self->{ctx}); $self->{git_commit_ok} = 1; $self->SUPER::close_edit(@_); } @@ -3781,6 +2925,8 @@ use strict; use warnings; use Carp qw/croak/; use IO::File; +use Git qw/command command_oneline command_noisy + command_output_pipe command_input_pipe command_close_pipe/; sub new { my $class = shift; @@ -3830,10 +2976,8 @@ sub rmdirs { delete $rm->{''}; # we never delete the url we're tracking return unless %$rm; - defined(my $pid = open my $fh,'-|') or croak $!; - if (!$pid) { - exec qw/git-ls-tree --name-only -r -z/, $self->{c} or croak $!; - } + my ($fh, $ctx) = command_output_pipe( + qw/ls-tree --name-only -r -z/, $self->{c}); local $/ = "\0"; while (<$fh>) { chomp; @@ -3842,11 +2986,11 @@ sub rmdirs { delete $rm->{join '/', @dn}; } unless (%$rm) { - close $fh; + eval { command_close_pipe($fh) }; return; } } - close $fh; + command_close_pipe($fh, $ctx); my ($r, $p, $bat) = ($self->{r}, $self->{pool}, $self->{bat}); foreach my $d (sort { $b =~ tr#/#/# <=> $a =~ tr#/#/# } keys %$rm) { @@ -4021,13 +3165,7 @@ __END__ Data structures: -$svn_log hashref (as returned by svn_log_raw) -{ - fh => file handle of the log file, - state => state of the log file parser (sep/msg/rev/msg_start...) -} - -$log_msg hashref as returned by next_log_entry($svn_log) +$log_msg hashref as returned by libsvn_log_entry() { msg => 'whitespace-formatted log entry ', # trailing newline is preserved @@ -4036,7 +3174,6 @@ $log_msg hashref as returned by next_log_entry($svn_log) author => 'committer name' }; - @mods = array of diff-index line hashes, each element represents one line of diff-index output diff --git a/git-tag.sh b/git-tag.sh index d53f94cd9c..e1bfa82f1e 100755 --- a/git-tag.sh +++ b/git-tag.sh @@ -40,11 +40,20 @@ do message="$1" if test "$#" = "0"; then die "error: option -m needs an argument" - exit 2 else message_given=1 fi ;; + -F) + annotate=1 + shift + if test "$#" = "0"; then + die "error: option -F needs an argument" + else + message="$(cat "$1")" + message_given=1 + fi + ;; -u) annotate=1 signed=1 @@ -59,8 +59,10 @@ static int handle_options(const char*** argv, int* argc) } else if (!strcmp(cmd, "-p") || !strcmp(cmd, "--paginate")) { setup_pager(); } else if (!strcmp(cmd, "--git-dir")) { - if (*argc < 1) - return -1; + if (*argc < 2) { + fprintf(stderr, "No directory given for --git-dir.\n" ); + usage(git_usage_string); + } setenv("GIT_DIR", (*argv)[1], 1); (*argv)++; (*argc)--; @@ -245,6 +247,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "push", cmd_push, RUN_SETUP }, { "read-tree", cmd_read_tree, RUN_SETUP }, { "repo-config", cmd_repo_config }, + { "rerere", cmd_rerere, RUN_SETUP }, { "rev-list", cmd_rev_list, RUN_SETUP }, { "rev-parse", cmd_rev_parse, RUN_SETUP }, { "rm", cmd_rm, RUN_SETUP }, diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index ebbc397ee8..d01d689348 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -128,6 +128,12 @@ our %feature = ( # => [content-encoding, suffix, program] 'default' => ['x-gzip', 'gz', 'gzip']}, + # Enable text search, which will list the commits which match author, + # committer or commit text to a given string. Enabled by default. + 'search' => { + 'override' => 0, + 'default' => [1]}, + # Enable the pickaxe search, which will list the commits that modified # a given string in a file. This can be practical and quite faster # alternative to 'blame', but still potentially CPU-intensive. @@ -351,6 +357,9 @@ if (defined $searchtext) { if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) { die_error(undef, "Invalid search parameter"); } + if (length($searchtext) < 2) { + die_error(undef, "At least two characters are required for search parameter"); + } $searchtext = quotemeta $searchtext; } @@ -1139,8 +1148,9 @@ sub git_get_last_activity { $git_dir = "$projectroot/$path"; open($fd, "-|", git_cmd(), 'for-each-ref', - '--format=%(refname) %(committer)', + '--format=%(committer)', '--sort=-committerdate', + '--count=1', 'refs/heads') or return; my $most_recent = <$fd>; close $fd or return; @@ -1726,6 +1736,9 @@ EOF print " / $action"; } print "\n"; + } + my ($have_search) = gitweb_check_feature('search'); + if ((defined $project) && ($have_search)) { if (!defined $searchtext) { $searchtext = ""; } @@ -2635,6 +2648,8 @@ sub git_shortlog_body { # uses global variable $project my ($revlist, $from, $to, $refs, $extra) = @_; + my $have_snapshot = gitweb_have_snapshot(); + $from = 0 unless defined $from; $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to); @@ -2662,7 +2677,7 @@ sub git_shortlog_body { $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " . $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"); - if (gitweb_have_snapshot()) { + if ($have_snapshot) { print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot"); } print "</td>\n" . @@ -2837,6 +2852,58 @@ sub git_heads_body { print "</table>\n"; } +sub git_search_grep_body { + my ($greplist, $from, $to, $extra) = @_; + $from = 0 unless defined $from; + $to = $#{$greplist} if (!defined $to || $#{$greplist} < $to); + + print "<table class=\"grep\" cellspacing=\"0\">\n"; + my $alternate = 1; + for (my $i = $from; $i <= $to; $i++) { + my $commit = $greplist->[$i]; + my %co = parse_commit($commit); + if (!%co) { + next; + } + if ($alternate) { + print "<tr class=\"dark\">\n"; + } else { + print "<tr class=\"light\">\n"; + } + $alternate ^= 1; + print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . + "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" . + "<td>" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, + esc_html(chop_str($co{'title'}, 50)) . "<br/>"); + my $comment = $co{'comment'}; + foreach my $line (@$comment) { + if ($line =~ m/^(.*)($searchtext)(.*)$/i) { + my $lead = esc_html($1) || ""; + $lead = chop_str($lead, 30, 10); + my $match = esc_html($2) || ""; + my $trail = esc_html($3) || ""; + $trail = chop_str($trail, 30, 10); + my $text = "$lead<span class=\"match\">$match</span>$trail"; + print chop_str($text, 80, 5) . "<br/>\n"; + } + } + print "</td>\n" . + "<td class=\"link\">" . + $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . + " | " . + $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); + print "</td>\n" . + "</tr>\n"; + } + if (defined $extra) { + print "<tr>\n" . + "<td colspan=\"3\">$extra</td>\n" . + "</tr>\n"; + } + print "</table>\n"; +} + ## ====================================================================== ## ====================================================================== ## actions @@ -2908,9 +2975,9 @@ sub git_project_index { sub git_summary { my $descr = git_get_project_description($project) || "none"; - my $head = git_get_head_hash($project); - my %co = parse_commit($head); + my %co = parse_commit("HEAD"); my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'}); + my $head = $co{'id'}; my $owner = git_get_project_owner($project); @@ -2957,7 +3024,7 @@ sub git_summary { # we need to request one more than 16 (0..15) to check if # those 16 are all open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17", - git_get_head_hash($project), "--" + $head, "--" or die_error(undef, "Open git-rev-list failed"); my @revlist = map { chomp; $_ } <$fd>; close $fd; @@ -2983,6 +3050,7 @@ sub git_summary { if (@forklist) { git_print_header_div('forks'); git_project_list_body(\@forklist, undef, 0, 15, + $#forklist <= 15 ? undef : $cgi->a({-href => href(action=>"forks")}, "..."), 'noheader'); } @@ -4144,6 +4212,10 @@ sub git_history { } sub git_search { + my ($have_search) = gitweb_check_feature('search'); + if (!$have_search) { + die_error('403 Permission denied', "Permission denied"); + } if (!defined $searchtext) { die_error(undef, "Text field empty"); } @@ -4154,6 +4226,9 @@ sub git_search { if (!%co) { die_error(undef, "Unknown commit object"); } + if (!defined $page) { + $page = 0; + } $searchtype ||= 'commit'; if ($searchtype eq 'pickaxe') { @@ -4166,66 +4241,68 @@ sub git_search { } git_header_html(); - git_print_page_nav('','', $hash,$co{'tree'},$hash); - git_print_header_div('commit', esc_html($co{'title'}), $hash); - print "<table cellspacing=\"0\">\n"; - my $alternate = 1; if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') { - $/ = "\0"; + my $greptype; + if ($searchtype eq 'commit') { + $greptype = "--grep="; + } elsif ($searchtype eq 'author') { + $greptype = "--author="; + } elsif ($searchtype eq 'committer') { + $greptype = "--committer="; + } open my $fd, "-|", git_cmd(), "rev-list", - "--header", "--parents", $hash, "--" + ("--max-count=" . (100 * ($page+1))), + ($greptype . $searchtext), + $hash, "--" or next; - while (my $commit_text = <$fd>) { - if (!grep m/$searchtext/i, $commit_text) { - next; - } - if ($searchtype eq 'author' && !grep m/\nauthor .*$searchtext/i, $commit_text) { - next; - } - if ($searchtype eq 'committer' && !grep m/\ncommitter .*$searchtext/i, $commit_text) { - next; - } - my @commit_lines = split "\n", $commit_text; - my %co = parse_commit(undef, \@commit_lines); - if (!%co) { - next; - } - if ($alternate) { - print "<tr class=\"dark\">\n"; - } else { - print "<tr class=\"light\">\n"; - } - $alternate ^= 1; - print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" . - "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" . - "<td>" . - $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"}, - esc_html(chop_str($co{'title'}, 50)) . "<br/>"); - my $comment = $co{'comment'}; - foreach my $line (@$comment) { - if ($line =~ m/^(.*)($searchtext)(.*)$/i) { - my $lead = esc_html($1) || ""; - $lead = chop_str($lead, 30, 10); - my $match = esc_html($2) || ""; - my $trail = esc_html($3) || ""; - $trail = chop_str($trail, 30, 10); - my $text = "$lead<span class=\"match\">$match</span>$trail"; - print chop_str($text, 80, 5) . "<br/>\n"; - } - } - print "</td>\n" . - "<td class=\"link\">" . - $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") . - " | " . - $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree"); - print "</td>\n" . - "</tr>\n"; - } + my @revlist = map { chomp; $_ } <$fd>; close $fd; + + my $paging_nav = ''; + if ($page > 0) { + $paging_nav .= + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype)}, + "first"); + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page-1), + -accesskey => "p", -title => "Alt-p"}, "prev"); + } else { + $paging_nav .= "first"; + $paging_nav .= " ⋅ prev"; + } + if ($#revlist >= (100 * ($page+1)-1)) { + $paging_nav .= " ⋅ " . + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } else { + $paging_nav .= " ⋅ next"; + } + my $next_link = ''; + if ($#revlist >= (100 * ($page+1)-1)) { + $next_link = + $cgi->a({-href => href(action=>"search", hash=>$hash, + searchtext=>$searchtext, searchtype=>$searchtype, + page=>$page+1), + -accesskey => "n", -title => "Alt-n"}, "next"); + } + + git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + git_search_grep_body(\@revlist, ($page * 100), $#revlist, $next_link); } if ($searchtype eq 'pickaxe') { + git_print_page_nav('','', $hash,$co{'tree'},$hash); + git_print_header_div('commit', esc_html($co{'title'}), $hash); + + print "<table cellspacing=\"0\">\n"; + my $alternate = 1; $/ = "\n"; my $git_command = git_cmd_str(); open my $fd, "-|", "$git_command rev-list $hash | " . @@ -4280,8 +4357,9 @@ sub git_search { } } close $fd; + + print "</table>\n"; } - print "</table>\n"; git_footer_html(); } diff --git a/imap-send.c b/imap-send.c index 894cbbdf53..ad91858bc4 100644 --- a/imap-send.c +++ b/imap-send.c @@ -96,8 +96,8 @@ typedef struct { static int Verbose, Quiet; -static void info( const char *, ... ); -static void warn( const char *, ... ); +static void imap_info( const char *, ... ); +static void imap_warn( const char *, ... ); static char *next_arg( char ** ); @@ -297,7 +297,7 @@ buffer_gets( buffer_t * b, char **s ) } static void -info( const char *msg, ... ) +imap_info( const char *msg, ... ) { va_list va; @@ -310,7 +310,7 @@ info( const char *msg, ... ) } static void -warn( const char *msg, ... ) +imap_warn( const char *msg, ... ) { va_list va; @@ -903,7 +903,7 @@ imap_open_store( imap_server_conf_t *srvc ) /* open connection to IMAP server */ if (srvc->tunnel) { - info( "Starting tunnel '%s'... ", srvc->tunnel ); + imap_info( "Starting tunnel '%s'... ", srvc->tunnel ); if (socketpair( PF_UNIX, SOCK_STREAM, 0, a )) { perror( "socketpair" ); @@ -926,31 +926,31 @@ imap_open_store( imap_server_conf_t *srvc ) imap->buf.sock.fd = a[1]; - info( "ok\n" ); + imap_info( "ok\n" ); } else { memset( &addr, 0, sizeof(addr) ); addr.sin_port = htons( srvc->port ); addr.sin_family = AF_INET; - info( "Resolving %s... ", srvc->host ); + imap_info( "Resolving %s... ", srvc->host ); he = gethostbyname( srvc->host ); if (!he) { perror( "gethostbyname" ); goto bail; } - info( "ok\n" ); + imap_info( "ok\n" ); addr.sin_addr.s_addr = *((int *) he->h_addr_list[0]); s = socket( PF_INET, SOCK_STREAM, 0 ); - info( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); + imap_info( "Connecting to %s:%hu... ", inet_ntoa( addr.sin_addr ), ntohs( addr.sin_port ) ); if (connect( s, (struct sockaddr *)&addr, sizeof(addr) )) { close( s ); perror( "connect" ); goto bail; } - info( "ok\n" ); + imap_info( "ok\n" ); imap->buf.sock.fd = s; @@ -979,7 +979,7 @@ imap_open_store( imap_server_conf_t *srvc ) if (!preauth) { - info ("Logging in...\n"); + imap_info ("Logging in...\n"); if (!srvc->user) { fprintf( stderr, "Skipping server %s, no user\n", srvc->host ); goto bail; @@ -1006,7 +1006,7 @@ imap_open_store( imap_server_conf_t *srvc ) fprintf( stderr, "Skipping account %s@%s, server forbids LOGIN\n", srvc->user, srvc->host ); goto bail; } - warn( "*** IMAP Warning *** Password is being sent in the clear\n" ); + imap_warn( "*** IMAP Warning *** Password is being sent in the clear\n" ); if (imap_exec( ctx, NULL, "LOGIN \"%s\" \"%s\"", srvc->user, srvc->pass ) != RESP_OK) { fprintf( stderr, "IMAP error: LOGIN failed\n" ); goto bail; diff --git a/log-tree.c b/log-tree.c index 8787df5cc6..35be33aaf7 100644 --- a/log-tree.c +++ b/log-tree.c @@ -114,6 +114,14 @@ void show_log(struct rev_info *opt, const char *sep) opt->loginfo = NULL; if (!opt->verbose_header) { + if (opt->left_right) { + if (commit->object.flags & BOUNDARY) + putchar('-'); + else if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), stdout); if (opt->parents) show_parents(commit, abbrev_commit); @@ -192,10 +200,20 @@ void show_log(struct rev_info *opt, const char *sep) opt->diffopt.stat_sep = buffer; } } else { - printf("%s%s%s", - diff_get_color(opt->diffopt.color_diff, DIFF_COMMIT), - opt->commit_format == CMIT_FMT_ONELINE ? "" : "commit ", - diff_unique_abbrev(commit->object.sha1, abbrev_commit)); + fputs(diff_get_color(opt->diffopt.color_diff, DIFF_COMMIT), + stdout); + if (opt->commit_format != CMIT_FMT_ONELINE) + fputs("commit ", stdout); + if (opt->left_right) { + if (commit->object.flags & BOUNDARY) + putchar('-'); + else if (commit->object.flags & SYMMETRIC_LEFT) + putchar('<'); + else + putchar('>'); + } + fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), + stdout); if (opt->parents) show_parents(commit, abbrev_commit); if (parent) diff --git a/merge-recursive.c b/merge-recursive.c index 1de273ea1e..ca4f19e34d 100644 --- a/merge-recursive.c +++ b/merge-recursive.c @@ -649,8 +649,8 @@ static struct merge_file_info merge_file(struct diff_filespec *o, char *name1, *name2; int merge_status; - name1 = xstrdup(mkpath("%s/%s", branch1, a->path)); - name2 = xstrdup(mkpath("%s/%s", branch2, b->path)); + 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); @@ -1263,6 +1263,18 @@ static struct commit *get_ref(const char *ref) return (struct commit *)object; } +static const char *better_branch_name(const char *branch) +{ + static char githead_env[8 + 40 + 1]; + char *name; + + if (strlen(branch) != 40) + return branch; + sprintf(githead_env, "GITHEAD_%s", branch); + name = getenv(githead_env); + return name ? name : branch; +} + int main(int argc, char *argv[]) { static const char *bases[2]; @@ -1272,7 +1284,7 @@ int main(int argc, char *argv[]) struct commit *result, *h1, *h2; git_config(git_default_config); /* core.filemode */ - original_index_file = getenv("GIT_INDEX_FILE"); + original_index_file = getenv(INDEX_ENVIRONMENT); if (!original_index_file) original_index_file = xstrdup(git_path("index")); @@ -1293,11 +1305,14 @@ int main(int argc, char *argv[]) branch1 = argv[++i]; branch2 = argv[++i]; - printf("Merging %s with %s\n", branch1, branch2); h1 = get_ref(branch1); h2 = get_ref(branch2); + branch1 = better_branch_name(branch1); + branch2 = better_branch_name(branch2); + printf("Merging %s with %s\n", branch1, branch2); + if (bases_count == 1) { struct commit *ancestor = get_ref(bases[0]); clean = merge(h1, h2, branch1, branch2, 0, ancestor, &result); diff --git a/revision.c b/revision.c index 5dc1e70260..e7eccd9180 100644 --- a/revision.c +++ b/revision.c @@ -343,6 +343,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) static void add_parents_to_list(struct rev_info *revs, struct commit *commit, struct commit_list **list) { struct commit_list *parent = commit->parents; + unsigned left_flag; if (commit->object.flags & ADDED) return; @@ -387,6 +388,7 @@ static void add_parents_to_list(struct rev_info *revs, struct commit *commit, st if (revs->no_walk) return; + left_flag = (commit->object.flags & SYMMETRIC_LEFT); parent = commit->parents; while (parent) { struct commit *p = parent->item; @@ -394,6 +396,7 @@ static void add_parents_to_list(struct rev_info *revs, struct commit *commit, st parent = parent->next; parse_commit(p); + p->object.flags |= left_flag; if (p->object.flags & SEEN) continue; p->object.flags |= SEEN; @@ -640,7 +643,7 @@ int handle_revision_arg(const char *arg, struct rev_info *revs, add_pending_commit_list(revs, exclude, flags_exclude); free_commit_list(exclude); - a->object.flags |= flags; + a->object.flags |= flags | SYMMETRIC_LEFT; } else a->object.flags |= flags_exclude; b->object.flags |= flags; @@ -854,6 +857,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch revs->boundary = 1; continue; } + if (!strcmp(arg, "--left-right")) { + revs->left_right = 1; + continue; + } if (!strcmp(arg, "--objects")) { revs->tag_objects = 1; revs->tree_objects = 1; diff --git a/revision.h b/revision.h index 81f522c35d..ec991e5c57 100644 --- a/revision.h +++ b/revision.h @@ -9,6 +9,7 @@ #define BOUNDARY (1u<<5) #define BOUNDARY_SHOW (1u<<6) #define ADDED (1u<<7) /* Parents already parsed and added? */ +#define SYMMETRIC_LEFT (1u<<8) struct rev_info; struct log_info; @@ -40,6 +41,7 @@ struct rev_info { limited:1, unpacked:1, /* see also ignore_packed below */ boundary:1, + left_right:1, parents:1; /* Diff flags */ diff --git a/t/Makefile b/t/Makefile index c9bd9a4690..250a19019c 100644 --- a/t/Makefile +++ b/t/Makefile @@ -23,13 +23,9 @@ clean: # we can test NO_OPTIMIZE_COMMITS independently of LC_ALL full-svn-test: - $(MAKE) $(TSVN) GIT_SVN_NO_LIB=0 GIT_SVN_DELTA_FETCH=1 \ - GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C - $(MAKE) $(TSVN) GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C - $(MAKE) $(TSVN) GIT_SVN_NO_LIB=1 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ - LC_ALL=en_US.UTF-8 - $(MAKE) $(TSVN) GIT_SVN_NO_LIB=0 GIT_SVN_NO_OPTIMIZE_COMMITS=0 \ - LC_ALL=en_US.UTF-8 + $(MAKE) $(TSVN) GIT_SVN_DELTA_FETCH=1 \ + GIT_SVN_NO_OPTIMIZE_COMMITS=1 LC_ALL=C + $(MAKE) $(TSVN) GIT_SVN_NO_OPTIMIZE_COMMITS=0 LC_ALL=en_US.UTF-8 .PHONY: $(T) clean .NOTPARALLEL: diff --git a/t/lib-git-svn.sh b/t/lib-git-svn.sh index 63c670304f..99ada71349 100644 --- a/t/lib-git-svn.sh +++ b/t/lib-git-svn.sh @@ -7,17 +7,18 @@ then exit fi -GIT_DIR=$PWD/.git -GIT_SVN_DIR=$GIT_DIR/svn/git-svn -SVN_TREE=$GIT_SVN_DIR/svn-tree - -perl -e 'use SVN::Core' >/dev/null 2>&1 +perl -e 'use SVN::Core; $SVN::Core::VERSION gt "1.1.0" or die' >/dev/null 2>&1 if test $? -ne 0 then - echo 'Perl SVN libraries not found, tests requiring those will be skipped' - GIT_SVN_NO_LIB=1 + test_expect_success 'Perl SVN libraries not found, skipping test' : + test_done + exit fi +GIT_DIR=$PWD/.git +GIT_SVN_DIR=$GIT_DIR/svn/git-svn +SVN_TREE=$GIT_SVN_DIR/svn-tree + svnadmin >/dev/null 2>&1 if test $? -ne 1 then diff --git a/t/t4200-rerere.sh b/t/t4200-rerere.sh new file mode 100755 index 0000000000..5ee5b23095 --- /dev/null +++ b/t/t4200-rerere.sh @@ -0,0 +1,154 @@ +#!/bin/sh +# +# Copyright (c) 2006 Johannes E. Schindelin +# + +test_description='git-rerere +' + +. ./test-lib.sh + +cat > a1 << EOF +Whether 'tis nobler in the mind to suffer +The slings and arrows of outrageous fortune, +Or to take arms against a sea of troubles, +And by opposing end them? To die: to sleep; +No more; and by a sleep to say we end +The heart-ache and the thousand natural shocks +That flesh is heir to, 'tis a consummation +Devoutly to be wish'd. +EOF + +git add a1 +git commit -q -a -m initial + +git checkout -b first +cat >> a1 << EOF +To die, to sleep; +To sleep: perchance to dream: ay, there's the rub; +For in that sleep of death what dreams may come +When we have shuffled off this mortal coil, +Must give us pause: there's the respect +That makes calamity of so long life; +EOF +git commit -q -a -m first + +git checkout -b second master +git show first:a1 | sed 's/To die, t/To die! T/' > a1 +git commit -q -a -m second + +# activate rerere +mkdir .git/rr-cache + +test_expect_failure 'conflicting merge' 'git pull . first' + +sha1=4f58849a60b4f969a2848966b6d02893b783e8fb +rr=.git/rr-cache/$sha1 +test_expect_success 'recorded preimage' "grep ======= $rr/preimage" + +test_expect_success 'no postimage or thisimage yet' \ + "test ! -f $rr/postimage -a ! -f $rr/thisimage" + +git show first:a1 > a1 + +cat > expect << EOF +--- a/a1 ++++ b/a1 +@@ -6,11 +6,7 @@ + The heart-ache and the thousand natural shocks + That flesh is heir to, 'tis a consummation + Devoutly to be wish'd. +-<<<<<<< +-To die! To sleep; +-======= + To die, to sleep; +->>>>>>> + To sleep: perchance to dream: ay, there's the rub; + For in that sleep of death what dreams may come + When we have shuffled off this mortal coil, +EOF + +git rerere diff > out + +test_expect_success 'rerere diff' 'diff -u expect out' + +cat > expect << EOF +a1 +EOF + +git rerere status > out + +test_expect_success 'rerere status' 'diff -u expect out' + +test_expect_success 'commit succeeds' \ + "git commit -q -a -m 'prefer first over second'" + +test_expect_success 'recorded postimage' "test -f $rr/postimage" + +git checkout -b third master +git show second^:a1 | sed 's/To die: t/To die! T/' > a1 +git commit -q -a -m third + +test_expect_failure 'another conflicting merge' 'git pull . first' + +git show first:a1 | sed 's/To die: t/To die! T/' > expect +test_expect_success 'rerere kicked in' "! grep ======= a1" + +test_expect_success 'rerere prefers first change' 'diff -u a1 expect' + +rm $rr/postimage +echo "$sha1 a1" | tr '\012' '\0' > .git/rr-cache/MERGE_RR + +test_expect_success 'rerere clear' 'git rerere clear' + +test_expect_success 'clear removed the directory' "test ! -d $rr" + +mkdir $rr +echo Hello > $rr/preimage +echo World > $rr/postimage + +sha2=4000000000000000000000000000000000000000 +rr2=.git/rr-cache/$sha2 +mkdir $rr2 +echo Hello > $rr2/preimage + +case "$(date -d @11111111 +%s 2>/dev/null)" in +[1-9]*) + # it is a recent GNU date. good. + now=$(date +%s) + almost_15_days_ago=$(($now+60-15*86400)) + just_over_15_days_ago=$(($now-1-15*86400)) + almost_60_days_ago=$(($now+60-60*86400)) + just_over_60_days_ago=$(($now-1-60*86400)) + predate1="$(date -d "@$almost_60_days_ago" +%c)" + predate2="$(date -d "@$almost_15_days_ago" +%c)" + postdate1="$(date -d "@$just_over_60_days_ago" +%c)" + postdate2="$(date -d "@$just_over_15_days_ago" +%c)" + ;; +*) + # it is not GNU date. oh, well. + predate1="$(date)" + predate2="$(date)" + postdate1='1 Oct 2006 00:00:00' + postdate2='1 Dec 2006 00:00:00' +esac + +touch -m -d "$predate1" $rr/preimage +touch -m -d "$predate2" $rr2/preimage + +test_expect_success 'garbage collection (part1)' 'git rerere gc' + +test_expect_success 'young records still live' \ + "test -f $rr/preimage -a -f $rr2/preimage" + +touch -m -d "$postdate1" $rr/preimage +touch -m -d "$postdate2" $rr2/preimage + +test_expect_success 'garbage collection (part2)' 'git rerere gc' + +test_expect_success 'old records rest in peace' \ + "test ! -f $rr/preimage -a ! -f $rr2/preimage" + +test_done + + diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index a11ab0ad41..90eeeba2a3 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -23,15 +23,14 @@ test_expect_success "clone and setup child repos" ' git clone . two && cd two && git repo-config branch.master.remote one && - { - echo "URL: ../one/.git/" - echo "Pull: refs/heads/master:refs/heads/one" - } >.git/remotes/one + git repo-config remote.one.url ../one/.git/ && + git repo-config remote.one.fetch refs/heads/master:refs/heads/one && cd .. && git clone . three && cd three && git repo-config branch.master.remote two && git repo-config branch.master.merge refs/heads/one && + mkdir -p .git/remotes && { echo "URL: ../two/.git/" echo "Pull: refs/heads/master:refs/heads/two" diff --git a/t/t6024-recursive-merge.sh b/t/t6024-recursive-merge.sh index 964010e764..69b18f7d81 100644 --- a/t/t6024-recursive-merge.sh +++ b/t/t6024-recursive-merge.sh @@ -59,18 +59,18 @@ GIT_AUTHOR_DATE="2006-12-12 23:00:08" git commit -m F test_expect_failure "combined merge conflicts" "git merge -m final G" cat > expect << EOF -<<<<<<< HEAD/a1 +<<<<<<< HEAD:a1 F ======= G ->>>>>>> 26f86b677eb03d4d956dbe108b29cb77061c1e73/a1 +>>>>>>> G:a1 EOF test_expect_success "result contains a conflict" "diff -u expect a1" git ls-files --stage > out cat > expect << EOF -100644 f16f906ab60483c100d1241dfc39868de9ec9fcb 1 a1 +100644 da056ce14a2241509897fa68bb2b3b6e6194ef9e 1 a1 100644 cf84443e49e1b366fac938711ddf4be2d4d1d9e9 2 a1 100644 fd7923529855d0b274795ae3349c5e0438333979 3 a1 EOF diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh index f9de232366..0edf19e48d 100755 --- a/t/t9100-git-svn-basic.sh +++ b/t/t9100-git-svn-basic.sh @@ -56,7 +56,7 @@ git update-index --add --remove dir/a/b/c/d/e/file dir/file file git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch && svn up $SVN_TREE && test -d $SVN_TREE/dir && test ! -d $SVN_TREE/dir/a" @@ -70,7 +70,7 @@ git update-index --add dir/file/file git commit -m "$name" test_expect_failure "$name" \ - 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch' \ + 'git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch' \ || true @@ -85,7 +85,7 @@ git update-index --add -- bar git commit -m "$name" test_expect_failure "$name" \ - 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch2' \ + 'git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch2' \ || true @@ -100,7 +100,7 @@ git-update-index --add bar/zzz/yyy git commit -m "$name" test_expect_failure "$name" \ - 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch3' \ + 'git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch3' \ || true @@ -115,7 +115,7 @@ git update-index --add -- dir git commit -m "$name" test_expect_failure "$name" \ - 'git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch4' \ + 'git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch4' \ || true @@ -127,7 +127,7 @@ git update-index exec.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch5 && svn up $SVN_TREE && test ! -x $SVN_TREE/exec.sh" @@ -138,7 +138,7 @@ git update-index exec.sh git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch5 && svn up $SVN_TREE && test -x $SVN_TREE/exec.sh" @@ -153,7 +153,7 @@ then git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch5 && svn up $SVN_TREE && test -L $SVN_TREE/exec.sh" @@ -164,7 +164,7 @@ then git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch5 && svn up $SVN_TREE && test -x $SVN_TREE/bar/zzz && test -L $SVN_TREE/exec-2.sh" @@ -177,7 +177,7 @@ then git commit -m "$name" test_expect_success "$name" \ - "git-svn commit --find-copies-harder --rmdir remotes/git-svn..mybranch5 && + "git-svn set-tree --find-copies-harder --rmdir remotes/git-svn..mybranch5 && svn up $SVN_TREE && test -f $SVN_TREE/exec-2.sh && test ! -L $SVN_TREE/exec-2.sh && @@ -192,7 +192,7 @@ then git update-index exec-2.sh git commit -m 'éï∏' export LC_ALL="$GIT_SVN_LC_ALL" - test_expect_success "$name" "git-svn commit HEAD" + test_expect_success "$name" "git-svn set-tree HEAD" unset LC_ALL else echo "UTF-8 locale not set, test skipped ($GIT_SVN_LC_ALL)" @@ -229,9 +229,7 @@ tree d667270a1f7b109f5eb3aaea21ede14b56bfdd6e tree 8f51f74cf0163afc9ad68a4b1537288c4558b5a4 EOF -if test -z "$GIT_SVN_NO_LIB" || test "$GIT_SVN_NO_LIB" -eq 0; then - echo tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 >> expected -fi +echo tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904 >> expected test_expect_success "$name" "diff -u a expected" diff --git a/t/t9101-git-svn-props.sh b/t/t9101-git-svn-props.sh index a5a235f100..5543b07f16 100755 --- a/t/t9101-git-svn-props.sh +++ b/t/t9101-git-svn-props.sh @@ -74,7 +74,7 @@ test_expect_success "$name" \ 'git checkout -b mybranch remotes/git-svn && echo Hi again >> kw.c && git commit -a -m "test keywoards ignoring" && - git-svn commit remotes/git-svn..mybranch && + git-svn set-tree remotes/git-svn..mybranch && git pull . remotes/git-svn' expect='/* $Id$ */' diff --git a/t/t9102-git-svn-deep-rmdir.sh b/t/t9102-git-svn-deep-rmdir.sh index d693d183c8..572aaedc06 100755 --- a/t/t9102-git-svn-deep-rmdir.sh +++ b/t/t9102-git-svn-deep-rmdir.sh @@ -21,7 +21,7 @@ test_expect_success 'mirror via git-svn' " test_expect_success 'Try a commit on rmdir' " git rm -f deeply/nested/directory/number/2/another && git commit -a -m 'remove another' && - git-svn commit --rmdir HEAD && + git-svn set-tree --rmdir HEAD && svn ls -R $svnrepo | grep ^deeply/nested/directory/number/1 " diff --git a/t/t9104-git-svn-follow-parent.sh b/t/t9104-git-svn-follow-parent.sh index 01488ff78a..8d2e2fec39 100755 --- a/t/t9104-git-svn-follow-parent.sh +++ b/t/t9104-git-svn-follow-parent.sh @@ -6,13 +6,6 @@ test_description='git-svn --follow-parent fetching' . ./lib-git-svn.sh -if test -n "$GIT_SVN_NO_LIB" && test "$GIT_SVN_NO_LIB" -ne 0 -then - echo 'Skipping: --follow-parent needs SVN libraries' - test_done - exit 0 -fi - test_expect_success 'initialize repo' " mkdir import && cd import && diff --git a/t/t9105-git-svn-commit-diff.sh b/t/t9105-git-svn-commit-diff.sh index 746c8277d0..6323c7e3ac 100755 --- a/t/t9105-git-svn-commit-diff.sh +++ b/t/t9105-git-svn-commit-diff.sh @@ -4,13 +4,6 @@ test_description='git-svn commit-diff' . ./lib-git-svn.sh -if test -n "$GIT_SVN_NO_LIB" && test "$GIT_SVN_NO_LIB" -ne 0 -then - echo 'Skipping: commit-diff needs SVN libraries' - test_done - exit 0 -fi - test_expect_success 'initialize repo' " mkdir import && cd import && diff --git a/t/t9106-git-svn-commit-diff-clobber.sh b/t/t9106-git-svn-commit-diff-clobber.sh index 58698b3f29..59b6425ce4 100755 --- a/t/t9106-git-svn-commit-diff-clobber.sh +++ b/t/t9106-git-svn-commit-diff-clobber.sh @@ -4,13 +4,6 @@ test_description='git-svn commit-diff clobber' . ./lib-git-svn.sh -if test -n "$GIT_SVN_NO_LIB" && test "$GIT_SVN_NO_LIB" -ne 0 -then - echo 'Skipping: commit-diff clobber needs SVN libraries' - test_done - exit 0 -fi - test_expect_success 'initialize repo' " mkdir import && cd import && diff --git a/t/test-lib.sh b/t/test-lib.sh index ac7be769b4..f0f9cd6be0 100755 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -208,8 +208,9 @@ test_done () { # t/ subdirectory and are run in trash subdirectory. PATH=$(pwd)/..:$PATH GIT_EXEC_PATH=$(pwd)/.. +GIT_TEMPLATE_DIR=$(pwd)/../templates/blt HOME=$(pwd)/trash -export PATH GIT_EXEC_PATH HOME +export PATH GIT_EXEC_PATH GIT_TEMPLATE_DIR HOME GITPERLLIB=$(pwd)/../perl/blib/lib:$(pwd)/../perl/blib/arch/auto/Git export GITPERLLIB diff --git a/templates/remotes-- b/templates/remotes-- deleted file mode 100644 index fae88709a6..0000000000 --- a/templates/remotes-- +++ /dev/null @@ -1 +0,0 @@ -: this is just to ensure the directory exists. @@ -29,12 +29,17 @@ static void error_builtin(const char *err, va_list params) report("error: ", err, params); } +static void warn_builtin(const char *warn, va_list params) +{ + report("warning: ", warn, params); +} /* If we are in a dlopen()ed .so write to a global variable would segfault * (ugh), so keep things static. */ static void (*usage_routine)(const char *err) NORETURN = usage_builtin; static void (*die_routine)(const char *err, va_list params) NORETURN = die_builtin; static void (*error_routine)(const char *err, va_list params) = error_builtin; +static void (*warn_routine)(const char *err, va_list params) = warn_builtin; void set_usage_routine(void (*routine)(const char *err) NORETURN) { @@ -51,6 +56,11 @@ void set_error_routine(void (*routine)(const char *err, va_list params)) error_routine = routine; } +void set_warn_routine(void (*routine)(const char *warn, va_list params)) +{ + warn_routine = routine; +} + void usage(const char *err) { @@ -75,3 +85,12 @@ int error(const char *err, ...) va_end(params); return -1; } + +void warn(const char *warn, ...) +{ + va_list params; + + va_start(params, warn); + warn_routine(warn, params); + va_end(params); +} diff --git a/utf8.c b/utf8.c new file mode 100644 index 0000000000..8fa62571aa --- /dev/null +++ b/utf8.c @@ -0,0 +1,278 @@ +#include "git-compat-util.h" +#include "utf8.h" + +/* This code is originally from http://www.cl.cam.ac.uk/~mgk25/ucs/ */ + +struct interval { + int first; + int last; +}; + +/* auxiliary function for binary search in interval table */ +static int bisearch(wchar_t ucs, const struct interval *table, int max) { + int min = 0; + int mid; + + if (ucs < table[0].first || ucs > table[max].last) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (ucs > table[mid].last) + min = mid + 1; + else if (ucs < table[mid].first) + max = mid - 1; + else + return 1; + } + + return 0; +} + +/* The following two functions define the column width of an ISO 10646 + * character as follows: + * + * - The null character (U+0000) has a column width of 0. + * + * - Other C0/C1 control characters and DEL will lead to a return + * value of -1. + * + * - Non-spacing and enclosing combining characters (general + * category code Mn or Me in the Unicode database) have a + * column width of 0. + * + * - SOFT HYPHEN (U+00AD) has a column width of 1. + * + * - Other format characters (general category code Cf in the Unicode + * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0. + * + * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF) + * have a column width of 0. + * + * - Spacing characters in the East Asian Wide (W) or East Asian + * Full-width (F) category as defined in Unicode Technical + * Report #11 have a column width of 2. + * + * - All remaining characters (including all printable + * ISO 8859-1 and WGL4 characters, Unicode control characters, + * etc.) have a column width of 1. + * + * This implementation assumes that wchar_t characters are encoded + * in ISO 10646. + */ + +static int wcwidth(wchar_t ch) +{ + /* + * Sorted list of non-overlapping intervals of non-spacing characters, + * generated by + * "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c". + */ + static const struct interval combining[] = { + { 0x0300, 0x0357 }, { 0x035D, 0x036F }, { 0x0483, 0x0486 }, + { 0x0488, 0x0489 }, { 0x0591, 0x05A1 }, { 0x05A3, 0x05B9 }, + { 0x05BB, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C4 }, { 0x0600, 0x0603 }, { 0x0610, 0x0615 }, + { 0x064B, 0x0658 }, { 0x0670, 0x0670 }, { 0x06D6, 0x06E4 }, + { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, { 0x070F, 0x070F }, + { 0x0711, 0x0711 }, { 0x0730, 0x074A }, { 0x07A6, 0x07B0 }, + { 0x0901, 0x0902 }, { 0x093C, 0x093C }, { 0x0941, 0x0948 }, + { 0x094D, 0x094D }, { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, + { 0x0981, 0x0981 }, { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, + { 0x09CD, 0x09CD }, { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, + { 0x0A3C, 0x0A3C }, { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, + { 0x0A4B, 0x0A4D }, { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, + { 0x0ABC, 0x0ABC }, { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, + { 0x0ACD, 0x0ACD }, { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, + { 0x0B3C, 0x0B3C }, { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, + { 0x0B4D, 0x0B4D }, { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, + { 0x0BC0, 0x0BC0 }, { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, + { 0x0C46, 0x0C48 }, { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, + { 0x0CBC, 0x0CBC }, { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, + { 0x0CCC, 0x0CCD }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, + { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, + { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, + { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, + { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x1712, 0x1714 }, + { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, { 0x1772, 0x1773 }, + { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, { 0x17C6, 0x17C6 }, + { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, { 0x180B, 0x180D }, + { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, { 0x1927, 0x1928 }, + { 0x1932, 0x1932 }, { 0x1939, 0x193B }, { 0x200B, 0x200F }, + { 0x202A, 0x202E }, { 0x2060, 0x2063 }, { 0x206A, 0x206F }, + { 0x20D0, 0x20EA }, { 0x302A, 0x302F }, { 0x3099, 0x309A }, + { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, { 0xFE20, 0xFE23 }, + { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, { 0x1D167, 0x1D169 }, + { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, + { 0x1D1AA, 0x1D1AD }, { 0xE0001, 0xE0001 }, + { 0xE0020, 0xE007F }, { 0xE0100, 0xE01EF } + }; + + /* test for 8-bit control characters */ + if (ch == 0) + return 0; + if (ch < 32 || (ch >= 0x7f && ch < 0xa0)) + return -1; + + /* binary search in table of non-spacing characters */ + if (bisearch(ch, combining, sizeof(combining) + / sizeof(struct interval) - 1)) + return 0; + + /* + * If we arrive here, ch is neither a combining nor a C0/C1 + * control character. + */ + + return 1 + + (ch >= 0x1100 && + /* Hangul Jamo init. consonants */ + (ch <= 0x115f || + ch == 0x2329 || ch == 0x232a || + /* CJK ... Yi */ + (ch >= 0x2e80 && ch <= 0xa4cf && + ch != 0x303f) || + /* Hangul Syllables */ + (ch >= 0xac00 && ch <= 0xd7a3) || + /* CJK Compatibility Ideographs */ + (ch >= 0xf900 && ch <= 0xfaff) || + /* CJK Compatibility Forms */ + (ch >= 0xfe30 && ch <= 0xfe6f) || + /* Fullwidth Forms */ + (ch >= 0xff00 && ch <= 0xff60) || + (ch >= 0xffe0 && ch <= 0xffe6) || + (ch >= 0x20000 && ch <= 0x2fffd) || + (ch >= 0x30000 && ch <= 0x3fffd))); +} + +/* + * This function returns the number of columns occupied by the character + * pointed to by the variable start. The pointer is updated to point at + * the next character. If it was not valid UTF-8, the pointer is set to NULL. + */ +int utf8_width(const char **start) +{ + unsigned char *s = (unsigned char *)*start; + wchar_t ch; + + if (*s < 0x80) { + /* 0xxxxxxx */ + ch = *s; + *start += 1; + } else if ((s[0] & 0xe0) == 0xc0) { + /* 110XXXXx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] & 0xfe) == 0xc0) + goto invalid; + ch = ((s[0] & 0x1f) << 6) | (s[1] & 0x3f); + *start += 2; + } else if ((s[0] & 0xf0) == 0xe0) { + /* 1110XXXX 10Xxxxxx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + (s[2] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) || + /* surrogate? */ + (s[0] == 0xed && (s[1] & 0xe0) == 0xa0) || + /* U+FFFE or U+FFFF? */ + (s[0] == 0xef && s[1] == 0xbf && + (s[2] & 0xfe) == 0xbe)) + goto invalid; + ch = ((s[0] & 0x0f) << 12) | + ((s[1] & 0x3f) << 6) | (s[2] & 0x3f); + *start += 3; + } else if ((s[0] & 0xf8) == 0xf0) { + /* 11110XXX 10XXxxxx 10xxxxxx 10xxxxxx */ + if ((s[1] & 0xc0) != 0x80 || + (s[2] & 0xc0) != 0x80 || + (s[3] & 0xc0) != 0x80 || + /* overlong? */ + (s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) || + /* > U+10FFFF? */ + (s[0] == 0xf4 && s[1] > 0x8f) || s[0] > 0xf4) + goto invalid; + ch = ((s[0] & 0x07) << 18) | ((s[1] & 0x3f) << 12) | + ((s[2] & 0x3f) << 6) | (s[3] & 0x3f); + *start += 4; + } else { +invalid: + *start = NULL; + return 0; + } + + return wcwidth(ch); +} + +int is_utf8(const char *text) +{ + while (*text) { + if (*text == '\n' || *text == '\t' || *text == '\r') { + text++; + continue; + } + utf8_width(&text); + if (!text) + return 0; + } + return 1; +} + +static void print_spaces(int count) +{ + static const char s[] = " "; + while (count >= sizeof(s)) { + fwrite(s, sizeof(s) - 1, 1, stdout); + count -= sizeof(s) - 1; + } + fwrite(s, count, 1, stdout); +} + +/* + * Wrap the text, if necessary. The variable indent is the indent for the + * first line, indent2 is the indent for all other lines. + */ +void print_wrapped_text(const char *text, int indent, int indent2, int width) +{ + int w = indent, assume_utf8 = is_utf8(text); + const char *bol = text, *space = NULL; + + for (;;) { + char c = *text; + if (!c || isspace(c)) { + if (w < width || !space) { + const char *start = bol; + if (space) + start = space; + else + print_spaces(indent); + fwrite(start, text - start, 1, stdout); + if (!c) { + putchar('\n'); + return; + } else if (c == '\t') + w |= 0x07; + space = text; + w++; + text++; + } + else { + putchar('\n'); + text = bol = space + 1; + space = NULL; + w = indent = indent2; + } + continue; + } + if (assume_utf8) + w += utf8_width(&text); + else { + w++; + text++; + } + } +} diff --git a/utf8.h b/utf8.h new file mode 100644 index 0000000000..a0d7f591ad --- /dev/null +++ b/utf8.h @@ -0,0 +1,8 @@ +#ifndef GIT_UTF8_H +#define GIT_UTF8_H + +int utf8_width(const char **start); +int is_utf8(const char *text); +void print_wrapped_text(const char *text, int indent, int indent2, int len); + +#endif diff --git a/xdiff-interface.c b/xdiff-interface.c index 08602f5221..6c1f99b149 100644 --- a/xdiff-interface.c +++ b/xdiff-interface.c @@ -102,3 +102,22 @@ int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf) } return 0; } + +int read_mmfile(mmfile_t *ptr, const char *filename) +{ + struct stat st; + FILE *f; + + if (stat(filename, &st)) + return error("Could not stat %s", filename); + if ((f = fopen(filename, "rb")) == NULL) + return error("Could not open %s", filename); + ptr->ptr = xmalloc(st.st_size); + if (fread(ptr->ptr, st.st_size, 1, f) != 1) + return error("Could not read %s", filename); + fclose(f); + ptr->size = st.st_size; + return 0; +} + + diff --git a/xdiff-interface.h b/xdiff-interface.h index 1346908bea..1918808081 100644 --- a/xdiff-interface.h +++ b/xdiff-interface.h @@ -17,5 +17,6 @@ int xdiff_outf(void *priv_, mmbuffer_t *mb, int nbuf); int parse_hunk_header(char *line, int len, int *ob, int *on, int *nb, int *nn); +int read_mmfile(mmfile_t *ptr, const char *filename); #endif |