--- Here's another update on the work in progress. At this point, the C version is almost complete, there's only a few issues left (look for FIXME in builtin-commit.c). I've added a commit test case which should be split out in a patch on its own, but the good news is that it successfully exercises most of the command line options and the C version passes. My plan for the remainder of the work is still to wrap up the last few pieces of functionality and then start taking this big patch apart in a number of more manageable pieces. However, the bulk of this work will still be a big patch that removes git-commit.sh and adds builtin-commit in one swoop. Kristian Makefile | 9 +- builtin-add.c | 14 +- builtin-commit-tree.c | 120 +++++--- builtin-commit.c | 746 +++++++++++++++++++++++++++++++++++++++++++++++++ builtin.h | 3 +- cache.h | 3 +- color.c | 18 +- color.h | 4 +- commit.h | 9 + git-commit.sh | 658 ------------------------------------------- git.c | 3 +- mktag.c | 8 +- sha1_file.c | 44 ++- t/t7800-commit.sh | 126 +++++++++ wt-status.c | 86 +++--- wt-status.h | 4 + 16 files changed, 1066 insertions(+), 789 deletions(-) create mode 100644 builtin-commit.c delete mode 100755 git-commit.sh create mode 100644 t/t7800-commit.sh diff --git a/Makefile b/Makefile index 0f75955..967d5a5 100644 --- a/Makefile +++ b/Makefile @@ -198,7 +198,7 @@ BASIC_LDFLAGS = SCRIPT_SH = \ git-bisect.sh git-checkout.sh \ - git-clean.sh git-clone.sh git-commit.sh \ + git-clean.sh git-clone.sh \ git-fetch.sh \ git-ls-remote.sh \ git-merge-one-file.sh git-mergetool.sh git-parse-remote.sh \ @@ -257,7 +257,7 @@ EXTRA_PROGRAMS = BUILT_INS = \ git-format-patch$X git-show$X git-whatchanged$X git-cherry$X \ git-get-tar-commit-id$X git-init$X git-repo-config$X \ - git-fsck-objects$X git-cherry-pick$X \ + git-fsck-objects$X git-cherry-pick$X git-status$X\ $(patsubst builtin-%.o,git-%$X,$(BUILTIN_OBJS)) # what 'all' will build and 'install' will install, in gitexecdir @@ -332,6 +332,7 @@ BUILTIN_OBJS = \ builtin-check-attr.o \ builtin-checkout-index.o \ builtin-check-ref-format.o \ + builtin-commit.o \ builtin-commit-tree.o \ builtin-count-objects.o \ builtin-describe.o \ @@ -367,7 +368,6 @@ BUILTIN_OBJS = \ builtin-rev-parse.o \ builtin-revert.o \ builtin-rm.o \ - builtin-runstatus.o \ builtin-shortlog.o \ builtin-show-branch.o \ builtin-stripspace.o \ @@ -791,9 +791,6 @@ $(patsubst %.perl,%,$(SCRIPT_PERL)): % : %.perl chmod +x $@+ && \ mv $@+ $@ -git-status: git-commit - $(QUIET_GEN)cp $< $@+ && mv $@+ $@ - gitweb/gitweb.cgi: gitweb/gitweb.perl $(QUIET_GEN)rm -f $@ $@+ && \ sed -e '1s|#!.*perl|#!$(PERL_PATH_SQ)|' \ diff --git a/builtin-add.c b/builtin-add.c index 1591171..bcd796d 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -8,6 +8,7 @@ #include "dir.h" #include "exec_cmd.h" #include "cache-tree.h" +#include "run-command.h" #include "diff.h" #include "diffcore.h" #include "commit.h" @@ -148,6 +149,13 @@ static int git_add_config(const char *var, const char *value) return git_default_config(var, value); } +int interactive_add(void) +{ + const char *argv[2] = { "add--interactive", NULL }; + + return run_command_v_opt(argv, RUN_GIT_CMD); +} + static struct lock_file lock_file; static const char ignore_warning[] = @@ -167,11 +175,9 @@ int cmd_add(int argc, const char **argv, const char *prefix) add_interactive++; } if (add_interactive) { - const char *args[] = { "add--interactive", NULL }; - - if (add_interactive != 1 || argc != 2) + if (argc != 2) die("add --interactive does not take any parameters"); - execv_git_cmd(args); + interactive_add(); exit(1); } diff --git a/builtin-commit-tree.c b/builtin-commit-tree.c index ccbcbe3..bb20470 100644 --- a/builtin-commit-tree.c +++ b/builtin-commit-tree.c @@ -20,17 +20,11 @@ static void init_buffer(char **bufp, unsigned int *sizep) *sizep = 0; } -static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) +static void add_chunk(char **bufp, unsigned int *sizep, const char *data, int len) { - char one_line[2048]; - va_list args; - int len; unsigned long alloc, size, newsize; char *buf; - va_start(args, fmt); - len = vsnprintf(one_line, sizeof(one_line), fmt, args); - va_end(args); size = *sizep; newsize = size + len + 1; alloc = (size + 32767) & ~32767; @@ -41,7 +35,19 @@ static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) *bufp = buf; } *sizep = newsize - 1; - memcpy(buf + size, one_line, len); + memcpy(buf + size, data, len); +} + +static void add_buffer(char **bufp, unsigned int *sizep, const char *fmt, ...) +{ + char one_line[2048]; + va_list args; + int len; + + va_start(args, fmt); + len = vsnprintf(one_line, sizeof(one_line), fmt, args); + va_end(args); + add_chunk(bufp, sizep, one_line, len); } static void check_valid(unsigned char *sha1, enum object_type expect) @@ -81,39 +87,16 @@ static const char commit_utf8_warn[] = "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) +const unsigned char * +create_commit(const unsigned char *tree_sha1, + unsigned char parent_sha1[][20], int parents, + const char *author_info, const char *committer_info, + const char *message, int length) { - int i; - int parents = 0; - unsigned char tree_sha1[20]; - unsigned char commit_sha1[20]; - char comment[1000]; + static unsigned char commit_sha1[20]; + int encoding_is_utf8, i; char *buffer; unsigned int size; - int encoding_is_utf8; - - git_config(git_default_config); - - if (argc < 2) - usage(commit_tree_usage); - if (get_sha1(argv[1], tree_sha1)) - die("Not a valid object name %s", argv[1]); - - check_valid(tree_sha1, OBJ_TREE); - for (i = 2; i < argc; i += 2) { - const char *a, *b; - 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], OBJ_COMMIT); - if (new_parent(parents)) - parents++; - } /* Not having i18n.commitencoding is the same as having utf-8 */ encoding_is_utf8 = is_encoding_utf8(git_commit_encoding); @@ -130,26 +113,71 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix) add_buffer(&buffer, &size, "parent %s\n", sha1_to_hex(parent_sha1[i])); /* Person/date information */ - add_buffer(&buffer, &size, "author %s\n", git_author_info(1)); - add_buffer(&buffer, &size, "committer %s\n", git_committer_info(1)); + add_buffer(&buffer, &size, "author %s\n", author_info); + add_buffer(&buffer, &size, "committer %s\n", committer_info); if (!encoding_is_utf8) add_buffer(&buffer, &size, "encoding %s\n", git_commit_encoding); add_buffer(&buffer, &size, "\n"); /* And add the comment */ - while (fgets(comment, sizeof(comment), stdin) != NULL) - add_buffer(&buffer, &size, "%s", comment); + add_chunk(&buffer, &size, message, length); /* And check the encoding */ buffer[size] = '\0'; if (encoding_is_utf8 && !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; + if (!write_sha1_file(buffer, size, commit_type, commit_sha1)) + return commit_sha1; + + return NULL; +} + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + int i; + int parents = 0; + unsigned char tree_sha1[20]; + char *buffer; + const unsigned char *commit_sha1; + unsigned long length; + + git_config(git_default_config); + + if (argc < 2) + usage(commit_tree_usage); + if (get_sha1(argv[1], tree_sha1)) + die("Not a valid object name %s", argv[1]); + + check_valid(tree_sha1, OBJ_TREE); + for (i = 2; i < argc; i += 2) { + const char *a, *b; + 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], OBJ_COMMIT); + if (new_parent(parents)) + parents++; } - else + + if (read_fd(0, &buffer, &length)) + die("Could not read commit message from standard input"); + + commit_sha1 = create_commit(tree_sha1, + parent_sha1, parents, + git_author_info(1), + git_committer_info(1), + buffer, length); + + if (!commit_sha1) return 1; + + printf("%s\n", sha1_to_hex(commit_sha1)); + return 0; } diff --git a/builtin-commit.c b/builtin-commit.c new file mode 100644 index 0000000..198d5af --- /dev/null +++ b/builtin-commit.c @@ -0,0 +1,746 @@ +/* + * Builtin "git commit" + * + * Copyright (c) 2007 Kristian Høgsberg <krh@xxxxxxxxxx> + * Based on git-commit.sh by Junio C Hamano and Linus Torvalds + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "cache.h" +#include "cache-tree.h" +#include "builtin.h" +#include "diff.h" +#include "diffcore.h" +#include "commit.h" +#include "revision.h" +#include "wt-status.h" +#include "run-command.h" +#include "refs.h" +#include "log-tree.h" + +static const char builtin_commit_usage[] = + "[-a | --interactive] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit> | --amend] [-u] [-e] [--author <author>] [[-i | -o] <path>...]"; + +static unsigned char head_sha1[20], merge_head_sha1[20]; +static struct commit *use_message_commit; +static const char commit_editmsg[] = "COMMIT_EDITMSG"; +static struct lock_file lock_file; + +enum option_type { + OPTION_NONE, + OPTION_STRING, + OPTION_INTEGER, + OPTION_LAST, +}; + +struct option { + enum option_type type; + const char *long_name; + char short_name; + void *value; +}; + +static int scan_options(const char ***argv, struct option *options) +{ + const char *value, *eq; + int i; + + if (**argv == NULL) + return 0; + if ((**argv)[0] != '-') + return 0; + if (!strcmp(**argv, "--")) + return 0; + + value = NULL; + for (i = 0; options[i].type != OPTION_LAST; i++) { + if ((**argv)[1] == '-') { + if (!prefixcmp(options[i].long_name, **argv + 2)) { + if (options[i].type != OPTION_NONE) + value = *++(*argv); + goto match; + } + + eq = strchr(**argv + 2, '='); + if (eq && options[i].type != OPTION_NONE && + !strncmp(**argv + 2, + options[i].long_name, eq - **argv - 2)) { + value = eq + 1; + goto match; + } + } + + if ((**argv)[1] == options[i].short_name) { + if ((**argv)[2] == '\0') { + if (options[i].type != OPTION_NONE) + value = *++(*argv); + goto match; + } + + if (options[i].type != OPTION_NONE) { + value = **argv + 2; + goto match; + } + } + } + + usage(builtin_commit_usage); + + match: + switch (options[i].type) { + case OPTION_NONE: + *(int *)options[i].value = 1; + break; + case OPTION_STRING: + if (value == NULL) + die("option %s requires a value.", (*argv)[-1]); + *(const char **)options[i].value = value; + break; + case OPTION_INTEGER: + if (value == NULL) + die("option %s requires a value.", (*argv)[-1]); + *(int *)options[i].value = atoi(value); + break; + default: + assert(0); + } + + (*argv)++; + + return 1; +} + +static char *logfile, *force_author, *message; +static char *edit_message, *use_message; +static int all, edit_flag, also, interactive, only, no_verify, amend, signoff; +static int quiet, verbose, untracked_files; + +static int no_edit, initial_commit, in_merge; +const char *only_include_assumed; + +static struct option commit_options[] = { + { OPTION_STRING, "file", 'F', (void *) &logfile }, + { OPTION_NONE, "all", 'a', &all }, + { OPTION_STRING, "author", 0, (void *) &force_author }, + { OPTION_NONE, "edit", 0, &edit_flag }, + { OPTION_NONE, "include", 'i', &also }, + { OPTION_NONE, "interactive", 0, &interactive }, + { OPTION_NONE, "only", 'o', &only }, + { OPTION_STRING, "message", 'm', &message }, + { OPTION_NONE, "no-verify", 'n', &no_verify }, + { OPTION_NONE, "amend", 0, &amend }, + { OPTION_STRING, "reedit-message", 'c', &edit_message }, + { OPTION_STRING, "reuse-message", 'C', &use_message }, + { OPTION_NONE, "signoff", 's', &signoff }, + { OPTION_NONE, "quiet", 'q', &quiet }, + { OPTION_NONE, "verbose", 'v', &verbose }, + { OPTION_NONE, "untracked-files", 0, &untracked_files }, + { OPTION_LAST }, +}; + +/* FIXME: Taken from builtin-add, should be shared. */ + +static void update_callback(struct diff_queue_struct *q, + struct diff_options *opt, void *cbdata) +{ + int i, verbose; + + verbose = *((int *)cbdata); + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + const char *path = p->one->path; + switch (p->status) { + default: + die("unexpacted diff status %c", p->status); + case DIFF_STATUS_UNMERGED: + case DIFF_STATUS_MODIFIED: + add_file_to_cache(path, verbose); + break; + case DIFF_STATUS_DELETED: + remove_file_from_cache(path); + if (verbose) + printf("remove '%s'\n", path); + break; + } + } +} + +static void +add_files_to_cache(int fd, const char **files, const char *prefix) +{ + struct rev_info rev; + + init_revisions(&rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.prune_data = get_pathspec(prefix, files); + rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = update_callback; + rev.diffopt.format_callback_data = &verbose; + + run_diff_files(&rev, 0); + + if (write_cache(fd, active_cache, active_nr) || close(fd)) + die("unable to write new index file"); +} + +static char * +prepare_index(const char **files, const char *prefix) +{ + int fd; + struct tree *tree; + struct lock_file *next_index_lock; + + fd = hold_locked_index(&lock_file, 1); + if (read_cache() < 0) + die("index file corrupt"); + + if (all) { + add_files_to_cache(fd, files, NULL); + return lock_file.filename; + } else if (also) { + add_files_to_cache(fd, files, prefix); + return lock_file.filename; + } + + if (interactive) + interactive_add(); + + if (*files == NULL) { + /* Commit index as-is. */ + rollback_lock_file(&lock_file); + return get_index_file(); + } + + /* + * FIXME: Warn on unknown files. Shell script does + * + * commit_only=`git-ls-files --error-unmatch -- "$@"` + */ + + /* + * FIXME: shell script does + * + * git-read-tree --index-output="$TMP_INDEX" -i -m HEAD + * + * which warns about unmerged files in the index. + */ + + /* update the user index file */ + add_files_to_cache(fd, files, prefix); + + tree = parse_tree_indirect(head_sha1); + if (!tree) + die("failed to unpack HEAD tree object"); + if (read_tree(tree, 0, NULL)) + die("failed to read HEAD tree object"); + + /* Uh oh, abusing lock_file to create a garbage collected file */ + next_index_lock = xmalloc(sizeof(*next_index_lock)); + fd = hold_lock_file_for_update(next_index_lock, + git_path("next-index-%d", getpid()), 1); + add_files_to_cache(fd, files, prefix); + + return next_index_lock->filename; +} + +static int strip_lines(char *buffer, int len) +{ + int blank_lines, i, j; + char *eol; + + blank_lines = 1; + for (i = 0, j = 0; i < len; i++) { + if (blank_lines > 0 && buffer[i] == '#') { + eol = strchr(buffer + i, '\n'); + if (!eol) + break; + + i = eol - buffer; + continue; + } + + if (buffer[i] == '\n') { + blank_lines++; + if (blank_lines > 1) + continue; + } else { + if (blank_lines > 2) + buffer[j++] = '\n'; + blank_lines = 0; + } + + buffer[j++] = buffer[i]; + } + + if (buffer[j - 1] != '\n') + buffer[j++] = '\n'; + + return j; +} + +static int run_status(FILE *fp, const char *index_file) +{ + struct wt_status s; + + wt_status_prepare(&s); + + if (amend) { + s.amend = 1; + s.reference = "HEAD^1"; + } + s.verbose = verbose; + s.untracked = untracked_files; + s.index_file = index_file; + s.fp = fp; + + wt_status_print(&s); + + return s.commitable; +} + +static const char sign_off_header[] = "Signed-off-by: "; + +static int prepare_log_message(const char *index_file) +{ + char *buffer = NULL; + struct stat statbuf; + int commitable; + unsigned long len; + FILE *fp; + + if (message) { + buffer = message; + len = strlen(message); + } else if (logfile && !strcmp(logfile, "-")) { + if (isatty(0)) + fprintf(stderr, "(reading log message from standard input)\n"); + if (read_fd(0, &buffer, &len)) + die("could not read log from standard input"); + } else if (logfile) { + if (read_path(logfile, &buffer, &len)) + die("could not read log file '%s': %s", + logfile, strerror(errno)); + } else if (use_message) { + /* FIXME: encoding */ + buffer = strstr(use_message_commit->buffer, "\n\n"); + if (!buffer || buffer[2] == '\0') + die("commit has empty message"); + buffer += 2; + len = strlen(buffer); + } else if (!stat(git_path("MERGE_MSG"), &statbuf)) { + if (read_path(git_path("MERGE_MSG"), &buffer, &len)) + die("could not read MERGE_MSG: %s", strerror(errno)); + } else if (!stat(git_path("SQUASH_MSG"), &statbuf)) { + if (read_path(git_path("SQUASH_MSG"), &buffer, &len)) + die("could not read SQUASH_MSG: %s", strerror(errno)); + } + + fp = fopen(git_path(commit_editmsg), "w"); + if (fp == NULL) + die("could not open %s\n", git_path(commit_editmsg)); + + if (buffer) { + len = strip_lines(buffer, len); + + if (fwrite(buffer, 1, len, fp) < len) + die("could not write commit template: %s\n", + strerror(errno)); + } + + if (signoff) { + const char *info, *bol; + + info = git_committer_info(1); + if (buffer) { + bol = strrchr(buffer + len - 1, '\n'); + if (!bol || prefixcmp(bol, sign_off_header)) + fprintf(fp, "\n"); + } + fprintf(fp, "Signed-off-by: %s\n", git_committer_info(1)); + } + + if (in_merge && !no_edit) { + fprintf(fp, + "#\n" + "# It looks like you may be committing a MERGE.\n" + "# If this is not correct, please remove the file\n" + "# %s\n" + "# and try again.\n" + "#\n", + git_path("MERGE_HEAD")); + } + + fprintf(fp, + "\n" + "# Please enter the commit message for your changes.\n" + "# (Comment lines starting with '#' will not be included)\n"); + if (only_include_assumed) + fprintf(fp, "# %s\n", only_include_assumed); + + commitable = run_status(fp, index_file); + + fclose(fp); + + return commitable; +} + +struct commit_info_strings { + const char *header, *name_env, *email_env, *date_env; +} author_info_strings = { + "\nauthor ", + "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_AUTHOR_DATE" +}, committer_info_strings = { + "\ncommitter ", + "GIT_COMMITTER_NAME", "GIT_COMMITER_EMAIL", "GIT_COMMITTER_DATE" +}; + +static char *determine_info(struct commit_info_strings *strings) +{ + char *p, *eol; + char *name = NULL, *email = NULL, *date = NULL; + + if (use_message) { + p = strstr(use_message_commit->buffer, strings->header); + if (!p) + die("invalid commit: %s\n", use_message); + p += strlen(strings->header); + eol = strchr(p, '\n'); + if (!eol) + die("invalid commit: %s\n", use_message); + + return xstrndup(p, eol - p); + } else if (force_author && strings == &author_info_strings) { + const char *eoname = strstr(force_author, " <"); + const char *eomail = strchr(force_author, '>'); + + if (!eoname || !eomail) + die("malformed --author parameter\n"); + name = xstrndup(force_author, eoname - force_author); + email = xstrndup(eoname + 2, eomail - eoname - 2); + } + + if (name == NULL) + name = getenv(strings->name_env); + if (email == NULL) + email = getenv(strings->email_env); + if (date == NULL) + date = getenv(strings->date_env); + + return xstrdup(fmt_ident(name, email, date, 1)); +} + +static void parse_and_validate_options(const char ***argv) +{ + int f = 0; + + git_config(git_status_config); + + (*argv)++; + while (scan_options(argv, commit_options)) + ; + + if (logfile || message || use_message) + no_edit = 1; + + if (get_sha1("HEAD", head_sha1)) + initial_commit = 1; + + if (!get_sha1("MERGE_HEAD", merge_head_sha1)) + in_merge = 1; + + /* Sanity check options */ + if (amend && initial_commit) + die("You have nothing to amend."); + if (amend && in_merge) + die("You are in the middle of a merger -- cannot amend."); + + if (use_message) + f++; + if (edit_message) + f++; + if (logfile) + f++; + if (amend) + f++; + if (f > 1) + die("Only one of -c/-C/-F/--amend can be used."); + if (message && f > 0) + die("Option -m cannot be combined with -c/-C/-F/--amend."); + if (edit_message) + use_message = edit_message; + if (amend) + use_message = "HEAD"; + if (use_message) { + unsigned char sha1[20]; + + if (get_sha1(use_message, sha1)) + die("could not lookup commit %s", use_message); + use_message_commit = lookup_commit(sha1); + if (!use_message_commit || parse_commit(use_message_commit)) + die("could not parse commit %s", use_message); + } + + if (also && only) + die("Only one of --include/--only can be used."); + if (!*argv && (also || (only && !amend))) + die("No paths with --include/--only does not make sense."); + if (!*argv && only && amend) + only_include_assumed = "Clever... amending the last one with dirty index."; + if (*argv && !also && !only) { + only_include_assumed = "Explicit paths specified without -i nor -o; assuming --only paths..."; + also = 0; + } + + if (all && interactive) + die("Cannot use -a, --interactive or -i at the same time."); + else if (all && **argv) + die("Paths with -a does not make sense."); + else if (interactive && **argv) + die("Paths with --interactive does not make sense."); +} + +int cmd_status(int argc, const char **argv, const char *prefix) +{ + const char *index_file; + int commitable; + + parse_and_validate_options(&argv); + + index_file = prepare_index(argv, prefix); + + commitable = run_status(stdout, index_file); + + rollback_lock_file(&lock_file); + + return commitable ? 0 : 1; +} + +static void launch_editor(const char *path) +{ + const char *editor, *terminal; + struct child_process child; + const char *args[3]; + + editor = getenv("VISUAL"); + if (!editor) + editor = getenv("EDITOR"); + + terminal = getenv("TERM"); + if (!editor && (!terminal || !strcmp(terminal, "dumb"))) { + fprintf(stderr, + "Terminal is dumb but no VISUAL nor EDITOR defined.\n" + "Please supply the commit log message using either\n" + "-m or -F option. A boilerplate log message has\n" + "been prepared in $GIT_DIR/COMMIT_EDITMSG\n"); + exit(1); + } + + if (!editor) + editor = "vi"; + + memset(&child, 0, sizeof(child)); + child.argv = args; + args[0] = editor; + args[1] = path; + args[2] = NULL; + + if (run_command(&child)) + die("could not launch editor %s.", editor); +} + +static int message_is_empty(const char *buffer, int len) +{ + static const char signed_off_by[] = "Signed-off-by: "; + const char *nl; + int eol, i; + + for (i = 0; i < len; i++) { + nl = memchr(buffer + i, '\n', len - i); + if (nl) + eol = nl - buffer; + else + eol = len; + + if (strlen(signed_off_by) <= eol - i && + !prefixcmp(buffer + i, signed_off_by)) { + i = eol; + continue; + } + while (i < eol) + if (!isspace(buffer[i++])) + return 0; + } + + return 1; +} + +static int run_hook(const char *index_file, const char *name, const char *arg) +{ + struct child_process hook; + const char *argv[3], *env[2]; + char index[PATH_MAX]; + + argv[0] = git_path("hooks/%s", name); + argv[1] = arg; + argv[2] = NULL; + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file); + env[0] = index; + env[1] = NULL; + + if (access(argv[0], X_OK) < 0) + return 0; + + memset(&hook, 0, sizeof(hook)); + hook.argv = argv; + hook.no_stdin = 1; + hook.stdout_to_stderr = 1; + hook.env = env; + + return run_command(&hook); +} + +static void print_summary(const char *prefix, const unsigned char *sha1) +{ + struct rev_info rev; + struct commit *commit; + + commit = lookup_commit(sha1); + if (!commit) + die("couldn't look up newly created commit\n"); + if (!commit || parse_commit(commit)) + die("could not parse newly created commit"); + + init_revisions(&rev, prefix); + setup_revisions(0, NULL, &rev, NULL); + + rev.abbrev = 0; + rev.diff = 1; + rev.diffopt.output_format = + DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY; + + rev.verbose_header = 1; + rev.show_root_diff = 1; + rev.commit_format = get_commit_format("format:%h: %s"); + rev.always_show_header = 1; + + printf("Created %scommit ", initial_commit ? "initial " : ""); + + log_tree_commit(&rev, commit); +} + +#define MAXPARENT (16) +static unsigned char parent_sha1[MAXPARENT][20]; + +int cmd_commit(int argc, const char **argv, const char *prefix) +{ + int parent_count = 0; + unsigned long len; + char *buffer; + const char *index_file, *reflog_msg; + const unsigned char *commit_sha1; + struct ref_lock *ref_lock; + + parse_and_validate_options(&argv); + + index_file = prepare_index(argv, prefix); + + if (run_hook(index_file, "pre-commit", NULL)) + exit(1); + + if (!prepare_log_message(index_file)) { + run_status(stdout, index_file); + unlink(commit_editmsg); + return 1; + } + + if (!no_edit) + launch_editor(git_path(commit_editmsg)); + + if (run_hook(index_file, "commit-msg", commit_editmsg)) + exit(1); + + if (read_path(git_path(commit_editmsg), &buffer, &len)) + die("could not read commit message file '%s': %s", + git_path(commit_editmsg), strerror(errno)); + + len = strip_lines(buffer, len); + + if (message_is_empty(buffer, len)) + die("* no commit message? aborting commit."); + + /* Determine parents */ + if (initial_commit) { + reflog_msg = "commit (initial)"; + parent_count = 0; + } else if (amend) { + struct commit_list *c; + struct commit *commit; + int i = 0; + + reflog_msg = "commit (amend)"; + commit = lookup_commit(head_sha1); + if (!commit || parse_commit(commit)) + die("could not parse HEAD commit"); + + for (c = commit->parents; c; c = c->next) { + hashcpy(parent_sha1[i++], c->item->object.sha1); + if (i == MAXPARENT) + die("Too many parents (%d max)", MAXPARENT); + } + parent_count = i; + } else if (in_merge) { + /* FIXME: how are merges with more than two parents handled? */ + reflog_msg = "commit (merge)"; + hashcpy(parent_sha1[0], head_sha1); + hashcpy(parent_sha1[1], merge_head_sha1); + parent_count = 2; + } else { + reflog_msg = "commit"; + hashcpy(parent_sha1[0], head_sha1); + parent_count = 1; + } + + read_cache_from(index_file); + active_cache_tree = cache_tree(); + if (cache_tree_update(active_cache_tree, + active_cache, active_nr, 0, 0) < 0) + die("Error building trees"); + + commit_sha1 = create_commit(active_cache_tree->sha1, + parent_sha1, parent_count, + determine_info(&author_info_strings), + determine_info(&committer_info_strings), + buffer, len); + + ref_lock = lock_any_ref_for_update("HEAD", + initial_commit ? NULL : head_sha1, + 0); + if (!ref_lock) + die("cannot lock HEAD ref"); + if (write_ref_sha1(ref_lock, commit_sha1, reflog_msg) < 0) + die("cannot update HEAD ref"); + + unlink(git_path("MERGE_HEAD")); + unlink(git_path("MERGE_MSG")); + + if (lock_file.filename[0] && commit_locked_index(&lock_file)) + die("failed to write new index"); + + /* + * FIXME: + * if test -d "$GIT_DIR/rr-cache" + * then + * git-rerere + * fi + */ + + run_hook(index_file, "post-commit", NULL); + + if (!quiet) + print_summary(prefix, commit_sha1); + + return 0; +} diff --git a/builtin.h b/builtin.h index da4834c..7f395da 100644 --- a/builtin.h +++ b/builtin.h @@ -23,6 +23,7 @@ extern int cmd_check_attr(int argc, const char **argv, const char *prefix); extern int cmd_check_ref_format(int argc, const char **argv, const char *prefix); extern int cmd_cherry(int argc, const char **argv, const char *prefix); extern int cmd_cherry_pick(int argc, const char **argv, const char *prefix); +extern int cmd_commit(int argc, const char **argv, const char *prefix); extern int cmd_commit_tree(int argc, const char **argv, const char *prefix); extern int cmd_count_objects(int argc, const char **argv, const char *prefix); extern int cmd_describe(int argc, const char **argv, const char *prefix); @@ -63,10 +64,10 @@ 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_revert(int argc, const char **argv, const char *prefix); extern int cmd_rm(int argc, const char **argv, const char *prefix); -extern int cmd_runstatus(int argc, const char **argv, const char *prefix); extern int cmd_shortlog(int argc, const char **argv, const char *prefix); extern int cmd_show(int argc, const char **argv, const char *prefix); extern int cmd_show_branch(int argc, const char **argv, const char *prefix); +extern int cmd_status(int argc, const char **argv, const char *prefix); extern int cmd_stripspace(int argc, const char **argv, const char *prefix); extern int cmd_symbolic_ref(int argc, const char **argv, const char *prefix); extern int cmd_tar_tree(int argc, const char **argv, const char *prefix); diff --git a/cache.h b/cache.h index 5e7381e..0403ada 100644 --- a/cache.h +++ b/cache.h @@ -245,7 +245,8 @@ extern int ie_match_stat(struct index_state *, struct cache_entry *, struct stat extern int ie_modified(struct index_state *, struct cache_entry *, struct stat *, int); extern int ce_path_match(const struct cache_entry *ce, const char **pathspec); extern int index_fd(unsigned char *sha1, int fd, struct stat *st, int write_object, enum object_type type, const char *path); -extern int read_pipe(int fd, char** return_buf, unsigned long* return_size); +extern int read_fd(int fd, char** return_buf, unsigned long* return_size); +extern int read_path(const char *path, char** return_buf, unsigned long* return_size); extern int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object); extern int index_path(unsigned char *sha1, const char *path, struct stat *st, int write_object); extern void fill_stat_cache_info(struct cache_entry *ce, struct stat *st); diff --git a/color.c b/color.c index 09d82ee..124ba33 100644 --- a/color.c +++ b/color.c @@ -135,39 +135,39 @@ int git_config_colorbool(const char *var, const char *value) return git_config_bool(var, value); } -static int color_vprintf(const char *color, const char *fmt, +static int color_vfprintf(FILE *fp, const char *color, const char *fmt, va_list args, const char *trail) { int r = 0; if (*color) - r += printf("%s", color); - r += vprintf(fmt, args); + r += fprintf(fp, "%s", color); + r += vfprintf(fp, fmt, args); if (*color) - r += printf("%s", COLOR_RESET); + r += fprintf(fp, "%s", COLOR_RESET); if (trail) - r += printf("%s", trail); + r += fprintf(fp, "%s", trail); return r; } -int color_printf(const char *color, const char *fmt, ...) +int color_fprintf(FILE *fp, const char *color, const char *fmt, ...) { va_list args; int r; va_start(args, fmt); - r = color_vprintf(color, fmt, args, NULL); + r = color_vfprintf(fp, color, fmt, args, NULL); va_end(args); return r; } -int color_printf_ln(const char *color, const char *fmt, ...) +int color_fprintf_ln(FILE *fp, const char *color, const char *fmt, ...) { va_list args; int r; va_start(args, fmt); - r = color_vprintf(color, fmt, args, "\n"); + r = color_vfprintf(fp, color, fmt, args, "\n"); va_end(args); return r; } diff --git a/color.h b/color.h index 88bb8ff..6809800 100644 --- a/color.h +++ b/color.h @@ -6,7 +6,7 @@ int git_config_colorbool(const char *var, const char *value); void color_parse(const char *var, const char *value, char *dst); -int color_printf(const char *color, const char *fmt, ...); -int color_printf_ln(const char *color, const char *fmt, ...); +int color_fprintf(FILE *fp, const char *color, const char *fmt, ...); +int color_fprintf_ln(FILE *fp, const char *color, const char *fmt, ...); #endif /* COLOR_H */ diff --git a/commit.h b/commit.h index a313b53..a07e379 100644 --- a/commit.h +++ b/commit.h @@ -122,4 +122,13 @@ extern struct commit_list *get_shallow_commits(struct object_array *heads, int depth, int shallow_flag, int not_shallow_flag); int in_merge_bases(struct commit *, struct commit **, int); + +const unsigned char * +create_commit(const unsigned char *tree_sha1, + unsigned char parent_sha1[][20], int parents, + const char *author_info, const char *committer_info, + const char *message, int length); + +int interactive_add(void); + #endif /* COMMIT_H */ diff --git a/git-commit.sh b/git-commit.sh deleted file mode 100755 index 5547a02..0000000 --- a/git-commit.sh +++ /dev/null @@ -1,658 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2005 Linus Torvalds -# Copyright (c) 2006 Junio C Hamano - -USAGE='[-a | --interactive] [-s] [-v] [--no-verify] [-m <message> | -F <logfile> | (-C|-c) <commit> | --amend] [-u] [-e] [--author <author>] [[-i | -o] <path>...]' -SUBDIRECTORY_OK=Yes -. git-sh-setup -require_work_tree - -git-rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=t - -case "$0" in -*status) - status_only=t - ;; -*commit) - status_only= - ;; -esac - -refuse_partial () { - echo >&2 "$1" - echo >&2 "You might have meant to say 'git commit -i paths...', perhaps?" - exit 1 -} - -THIS_INDEX="$GIT_DIR/index" -NEXT_INDEX="$GIT_DIR/next-index$$" -rm -f "$NEXT_INDEX" -save_index () { - cp -p "$THIS_INDEX" "$NEXT_INDEX" -} - -run_status () { - # If TMP_INDEX is defined, that means we are doing - # "--only" partial commit, and that index file is used - # to build the tree for the commit. Otherwise, if - # NEXT_INDEX exists, that is the index file used to - # make the commit. Otherwise we are using as-is commit - # so the regular index file is what we use to compare. - if test '' != "$TMP_INDEX" - then - GIT_INDEX_FILE="$TMP_INDEX" - export GIT_INDEX_FILE - elif test -f "$NEXT_INDEX" - then - GIT_INDEX_FILE="$NEXT_INDEX" - export GIT_INDEX_FILE - fi - - case "$status_only" in - t) color= ;; - *) color=--nocolor ;; - esac - git-runstatus ${color} \ - ${verbose:+--verbose} \ - ${amend:+--amend} \ - ${untracked_files:+--untracked} -} - -trap ' - test -z "$TMP_INDEX" || { - test -f "$TMP_INDEX" && rm -f "$TMP_INDEX" - } - rm -f "$NEXT_INDEX" -' 0 - -################################################################ -# Command line argument parsing and sanity checking - -all= -also= -interactive= -only= -logfile= -use_commit= -amend= -edit_flag= -no_edit= -log_given= -log_message= -verify=t -quiet= -verbose= -signoff= -force_author= -only_include_assumed= -untracked_files= -while case "$#" in 0) break;; esac -do - case "$1" in - -F|--F|-f|--f|--fi|--fil|--file) - case "$#" in 1) usage ;; esac - shift - no_edit=t - log_given=t$log_given - logfile="$1" - shift - ;; - -F*|-f*) - no_edit=t - log_given=t$log_given - logfile=`expr "z$1" : 'z-[Ff]\(.*\)'` - shift - ;; - --F=*|--f=*|--fi=*|--fil=*|--file=*) - no_edit=t - log_given=t$log_given - logfile=`expr "z$1" : 'z-[^=]*=\(.*\)'` - shift - ;; - -a|--a|--al|--all) - all=t - shift - ;; - --au=*|--aut=*|--auth=*|--autho=*|--author=*) - force_author=`expr "z$1" : 'z-[^=]*=\(.*\)'` - shift - ;; - --au|--aut|--auth|--autho|--author) - case "$#" in 1) usage ;; esac - shift - force_author="$1" - shift - ;; - -e|--e|--ed|--edi|--edit) - edit_flag=t - shift - ;; - -i|--i|--in|--inc|--incl|--inclu|--includ|--include) - also=t - shift - ;; - --int|--inte|--inter|--intera|--interac|--interact|--interacti|\ - --interactiv|--interactive) - interactive=t - shift - ;; - -o|--o|--on|--onl|--only) - only=t - shift - ;; - -m|--m|--me|--mes|--mess|--messa|--messag|--message) - case "$#" in 1) usage ;; esac - shift - log_given=m$log_given - if test "$log_message" = '' - then - log_message="$1" - else - log_message="$log_message - -$1" - fi - no_edit=t - shift - ;; - -m*) - log_given=m$log_given - if test "$log_message" = '' - then - log_message=`expr "z$1" : 'z-m\(.*\)'` - else - log_message="$log_message - -`expr "z$1" : 'z-m\(.*\)'`" - fi - no_edit=t - shift - ;; - --m=*|--me=*|--mes=*|--mess=*|--messa=*|--messag=*|--message=*) - log_given=m$log_given - if test "$log_message" = '' - then - log_message=`expr "z$1" : 'z-[^=]*=\(.*\)'` - else - log_message="$log_message - -`expr "z$1" : 'zq-[^=]*=\(.*\)'`" - fi - no_edit=t - shift - ;; - -n|--n|--no|--no-|--no-v|--no-ve|--no-ver|--no-veri|--no-verif|\ - --no-verify) - verify= - shift - ;; - --a|--am|--ame|--amen|--amend) - amend=t - log_given=t$log_given - use_commit=HEAD - shift - ;; - -c) - case "$#" in 1) usage ;; esac - shift - log_given=t$log_given - use_commit="$1" - no_edit= - shift - ;; - --ree=*|--reed=*|--reedi=*|--reedit=*|--reedit-=*|--reedit-m=*|\ - --reedit-me=*|--reedit-mes=*|--reedit-mess=*|--reedit-messa=*|\ - --reedit-messag=*|--reedit-message=*) - log_given=t$log_given - use_commit=`expr "z$1" : 'z-[^=]*=\(.*\)'` - no_edit= - shift - ;; - --ree|--reed|--reedi|--reedit|--reedit-|--reedit-m|--reedit-me|\ - --reedit-mes|--reedit-mess|--reedit-messa|--reedit-messag|\ - --reedit-message) - case "$#" in 1) usage ;; esac - shift - log_given=t$log_given - use_commit="$1" - no_edit= - shift - ;; - -C) - case "$#" in 1) usage ;; esac - shift - log_given=t$log_given - use_commit="$1" - no_edit=t - shift - ;; - --reu=*|--reus=*|--reuse=*|--reuse-=*|--reuse-m=*|--reuse-me=*|\ - --reuse-mes=*|--reuse-mess=*|--reuse-messa=*|--reuse-messag=*|\ - --reuse-message=*) - log_given=t$log_given - use_commit=`expr "z$1" : 'z-[^=]*=\(.*\)'` - no_edit=t - shift - ;; - --reu|--reus|--reuse|--reuse-|--reuse-m|--reuse-me|--reuse-mes|\ - --reuse-mess|--reuse-messa|--reuse-messag|--reuse-message) - case "$#" in 1) usage ;; esac - shift - log_given=t$log_given - use_commit="$1" - no_edit=t - shift - ;; - -s|--s|--si|--sig|--sign|--signo|--signof|--signoff) - signoff=t - shift - ;; - -q|--q|--qu|--qui|--quie|--quiet) - quiet=t - shift - ;; - -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) - verbose=t - shift - ;; - -u|--u|--un|--unt|--untr|--untra|--untrac|--untrack|--untracke|\ - --untracked|--untracked-|--untracked-f|--untracked-fi|--untracked-fil|\ - --untracked-file|--untracked-files) - untracked_files=t - shift - ;; - --) - shift - break - ;; - -*) - usage - ;; - *) - break - ;; - esac -done -case "$edit_flag" in t) no_edit= ;; esac - -################################################################ -# Sanity check options - -case "$amend,$initial_commit" in -t,t) - die "You do not have anything to amend." ;; -t,) - if [ -f "$GIT_DIR/MERGE_HEAD" ]; then - die "You are in the middle of a merge -- cannot amend." - fi ;; -esac - -case "$log_given" in -tt*) - die "Only one of -c/-C/-F/--amend can be used." ;; -*tm*|*mt*) - die "Option -m cannot be combined with -c/-C/-F/--amend." ;; -esac - -case "$#,$also,$only,$amend" in -*,t,t,*) - die "Only one of --include/--only can be used." ;; -0,t,,* | 0,,t,) - die "No paths with --include/--only does not make sense." ;; -0,,t,t) - only_include_assumed="# Clever... amending the last one with dirty index." ;; -0,,,*) - ;; -*,,,*) - only_include_assumed="# Explicit paths specified without -i nor -o; assuming --only paths..." - also= - ;; -esac -unset only -case "$all,$interactive,$also,$#" in -*t,*t,*) - die "Cannot use -a, --interactive or -i at the same time." ;; -t,,[1-9]*) - die "Paths with -a does not make sense." ;; -,t,[1-9]*) - die "Paths with --interactive does not make sense." ;; -,,t,0) - die "No paths with -i does not make sense." ;; -esac - -################################################################ -# Prepare index to have a tree to be committed - -case "$all,$also" in -t,) - if test ! -f "$THIS_INDEX" - then - die 'nothing to commit (use "git add file1 file2" to include for commit)' - fi - save_index && - ( - cd_to_toplevel && - GIT_INDEX_FILE="$NEXT_INDEX" && - export GIT_INDEX_FILE && - git-diff-files --name-only -z | - git-update-index --remove -z --stdin - ) || exit - ;; -,t) - save_index && - git-ls-files --error-unmatch -- "$@" >/dev/null || exit - - git-diff-files --name-only -z -- "$@" | - ( - cd_to_toplevel && - GIT_INDEX_FILE="$NEXT_INDEX" && - export GIT_INDEX_FILE && - git-update-index --remove -z --stdin - ) || exit - ;; -,) - if test "$interactive" = t; then - git add --interactive || exit - fi - case "$#" in - 0) - ;; # commit as-is - *) - if test -f "$GIT_DIR/MERGE_HEAD" - then - refuse_partial "Cannot do a partial commit during a merge." - fi - TMP_INDEX="$GIT_DIR/tmp-index$$" - commit_only=`git-ls-files --error-unmatch -- "$@"` || exit - - # Build a temporary index and update the real index - # the same way. - if test -z "$initial_commit" - then - GIT_INDEX_FILE="$THIS_INDEX" \ - git-read-tree --index-output="$TMP_INDEX" -i -m HEAD - else - rm -f "$TMP_INDEX" - fi || exit - - printf '%s\n' "$commit_only" | - GIT_INDEX_FILE="$TMP_INDEX" \ - git-update-index --add --remove --stdin && - - save_index && - printf '%s\n' "$commit_only" | - ( - GIT_INDEX_FILE="$NEXT_INDEX" - export GIT_INDEX_FILE - git-update-index --remove --stdin - ) || exit - ;; - esac - ;; -esac - -################################################################ -# If we do as-is commit, the index file will be THIS_INDEX, -# otherwise NEXT_INDEX after we make this commit. We leave -# the index as is if we abort. - -if test -f "$NEXT_INDEX" -then - USE_INDEX="$NEXT_INDEX" -else - USE_INDEX="$THIS_INDEX" -fi - -case "$status_only" in -t) - # This will silently fail in a read-only repository, which is - # what we want. - GIT_INDEX_FILE="$USE_INDEX" git-update-index -q --unmerged --refresh - run_status - exit $? - ;; -'') - GIT_INDEX_FILE="$USE_INDEX" git-update-index -q --refresh || exit - ;; -esac - -################################################################ -# Grab commit message, write out tree and make commit. - -if test t = "$verify" && test -x "$GIT_DIR"/hooks/pre-commit -then - if test "$TMP_INDEX" - then - GIT_INDEX_FILE="$TMP_INDEX" "$GIT_DIR"/hooks/pre-commit - else - GIT_INDEX_FILE="$USE_INDEX" "$GIT_DIR"/hooks/pre-commit - fi || exit -fi - -if test "$log_message" != '' -then - printf '%s\n' "$log_message" -elif test "$logfile" != "" -then - if test "$logfile" = - - then - test -t 0 && - echo >&2 "(reading log message from standard input)" - cat - else - cat <"$logfile" - fi -elif test "$use_commit" != "" -then - encoding=$(git config i18n.commitencoding || echo UTF-8) - git show -s --pretty=raw --encoding="$encoding" "$use_commit" | - sed -e '1,/^$/d' -e 's/^ //' -elif test -f "$GIT_DIR/MERGE_MSG" -then - cat "$GIT_DIR/MERGE_MSG" -elif test -f "$GIT_DIR/SQUASH_MSG" -then - cat "$GIT_DIR/SQUASH_MSG" -fi | git-stripspace >"$GIT_DIR"/COMMIT_EDITMSG - -case "$signoff" in -t) - need_blank_before_signoff= - tail -n 1 "$GIT_DIR"/COMMIT_EDITMSG | - grep 'Signed-off-by:' >/dev/null || need_blank_before_signoff=yes - { - test -z "$need_blank_before_signoff" || echo - git-var GIT_COMMITTER_IDENT | sed -e ' - s/>.*/>/ - s/^/Signed-off-by: / - ' - } >>"$GIT_DIR"/COMMIT_EDITMSG - ;; -esac - -if test -f "$GIT_DIR/MERGE_HEAD" && test -z "$no_edit"; then - echo "#" - echo "# It looks like you may be committing a MERGE." - echo "# If this is not correct, please remove the file" - printf '%s\n' "# $GIT_DIR/MERGE_HEAD" - echo "# and try again" - echo "#" -fi >>"$GIT_DIR"/COMMIT_EDITMSG - -# Author -if test '' != "$use_commit" -then - pick_author_script=' - /^author /{ - s/'\''/'\''\\'\'\''/g - h - s/^author \([^<]*\) <[^>]*> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_NAME='\''&'\''/p - - g - s/^author [^<]* <\([^>]*\)> .*$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_EMAIL='\''&'\''/p - - g - s/^author [^<]* <[^>]*> \(.*\)$/\1/ - s/'\''/'\''\'\'\''/g - s/.*/GIT_AUTHOR_DATE='\''&'\''/p - - q - } - ' - encoding=$(git config i18n.commitencoding || echo UTF-8) - set_author_env=`git show -s --pretty=raw --encoding="$encoding" "$use_commit" | - LANG=C LC_ALL=C sed -ne "$pick_author_script"` - eval "$set_author_env" - export GIT_AUTHOR_NAME - export GIT_AUTHOR_EMAIL - export GIT_AUTHOR_DATE -fi -if test '' != "$force_author" -then - GIT_AUTHOR_NAME=`expr "z$force_author" : 'z\(.*[^ ]\) *<.*'` && - GIT_AUTHOR_EMAIL=`expr "z$force_author" : '.*\(<.*\)'` && - test '' != "$GIT_AUTHOR_NAME" && - test '' != "$GIT_AUTHOR_EMAIL" || - die "malformed --author parameter" - export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL -fi - -PARENTS="-p HEAD" -if test -z "$initial_commit" -then - rloga='commit' - if [ -f "$GIT_DIR/MERGE_HEAD" ]; then - rloga='commit (merge)' - PARENTS="-p HEAD "`sed -e 's/^/-p /' "$GIT_DIR/MERGE_HEAD"` - elif test -n "$amend"; then - rloga='commit (amend)' - PARENTS=$(git-cat-file commit HEAD | - sed -n -e '/^$/q' -e 's/^parent /-p /p') - fi - current="$(git-rev-parse --verify HEAD)" -else - if [ -z "$(git-ls-files)" ]; then - echo >&2 'nothing to commit (use "git add file1 file2" to include for commit)' - exit 1 - fi - PARENTS="" - rloga='commit (initial)' - current='' -fi -set_reflog_action "$rloga" - -if test -z "$no_edit" -then - { - echo "" - echo "# Please enter the commit message for your changes." - echo "# (Comment lines starting with '#' will not be included)" - test -z "$only_include_assumed" || echo "$only_include_assumed" - run_status - } >>"$GIT_DIR"/COMMIT_EDITMSG -else - # we need to check if there is anything to commit - run_status >/dev/null -fi -if [ "$?" != "0" -a ! -f "$GIT_DIR/MERGE_HEAD" -a -z "$amend" ] -then - rm -f "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" - run_status - exit 1 -fi - -case "$no_edit" in -'') - case "${VISUAL:-$EDITOR},$TERM" in - ,dumb) - echo >&2 "Terminal is dumb but no VISUAL nor EDITOR defined." - echo >&2 "Please supply the commit log message using either" - echo >&2 "-m or -F option. A boilerplate log message has" - echo >&2 "been prepared in $GIT_DIR/COMMIT_EDITMSG" - exit 1 - ;; - esac - git-var GIT_AUTHOR_IDENT > /dev/null || die - git-var GIT_COMMITTER_IDENT > /dev/null || die - ${VISUAL:-${EDITOR:-vi}} "$GIT_DIR/COMMIT_EDITMSG" - ;; -esac - -case "$verify" in -t) - if test -x "$GIT_DIR"/hooks/commit-msg - then - "$GIT_DIR"/hooks/commit-msg "$GIT_DIR"/COMMIT_EDITMSG || exit - fi -esac - -if test -z "$no_edit" -then - sed -e ' - /^diff --git a\/.*/{ - s/// - q - } - /^#/d - ' "$GIT_DIR"/COMMIT_EDITMSG -else - cat "$GIT_DIR"/COMMIT_EDITMSG -fi | -git-stripspace >"$GIT_DIR"/COMMIT_MSG - -if cnt=`grep -v -i '^Signed-off-by' "$GIT_DIR"/COMMIT_MSG | - git-stripspace | - wc -l` && - test 0 -lt $cnt -then - if test -z "$TMP_INDEX" - then - tree=$(GIT_INDEX_FILE="$USE_INDEX" git-write-tree) - else - tree=$(GIT_INDEX_FILE="$TMP_INDEX" git-write-tree) && - rm -f "$TMP_INDEX" - fi && - commit=$(cat "$GIT_DIR"/COMMIT_MSG | git-commit-tree $tree $PARENTS) && - rlogm=$(sed -e 1q "$GIT_DIR"/COMMIT_MSG) && - git-update-ref -m "$GIT_REFLOG_ACTION: $rlogm" HEAD $commit "$current" && - rm -f -- "$GIT_DIR/MERGE_HEAD" "$GIT_DIR/MERGE_MSG" && - if test -f "$NEXT_INDEX" - then - mv "$NEXT_INDEX" "$THIS_INDEX" - else - : ;# happy - fi -else - echo >&2 "* no commit message? aborting commit." - false -fi -ret="$?" -rm -f "$GIT_DIR/COMMIT_MSG" "$GIT_DIR/COMMIT_EDITMSG" "$GIT_DIR/SQUASH_MSG" - -cd_to_toplevel - -if test -d "$GIT_DIR/rr-cache" -then - git-rerere -fi - -if test "$ret" = 0 -then - if test -x "$GIT_DIR"/hooks/post-commit - then - "$GIT_DIR"/hooks/post-commit - fi - if test -z "$quiet" - then - commit=`git-diff-tree --always --shortstat --pretty="format:%h: %s"\ - --summary --root HEAD --` - echo "Created${initial_commit:+ initial} commit $commit" - fi -fi - -exit "$ret" diff --git a/git.c b/git.c index 29b55a1..4018e3c 100644 --- a/git.c +++ b/git.c @@ -237,6 +237,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "check-attr", cmd_check_attr, RUN_SETUP | NOT_BARE }, { "cherry", cmd_cherry, RUN_SETUP }, { "cherry-pick", cmd_cherry_pick, RUN_SETUP | NOT_BARE }, + { "commit", cmd_commit, RUN_SETUP }, { "commit-tree", cmd_commit_tree, RUN_SETUP }, { "config", cmd_config }, { "count-objects", cmd_count_objects, RUN_SETUP }, @@ -279,10 +280,10 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "rev-parse", cmd_rev_parse, RUN_SETUP }, { "revert", cmd_revert, RUN_SETUP | NOT_BARE }, { "rm", cmd_rm, RUN_SETUP | NOT_BARE }, - { "runstatus", cmd_runstatus, RUN_SETUP | NOT_BARE }, { "shortlog", cmd_shortlog, RUN_SETUP | USE_PAGER }, { "show-branch", cmd_show_branch, RUN_SETUP }, { "show", cmd_show, RUN_SETUP | USE_PAGER }, + { "status", cmd_status, RUN_SETUP | NOT_BARE }, { "stripspace", cmd_stripspace }, { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, { "tar-tree", cmd_tar_tree }, diff --git a/mktag.c b/mktag.c index b82e377..26b9ebf 100644 --- a/mktag.c +++ b/mktag.c @@ -111,8 +111,8 @@ static int verify_tag(char *buffer, unsigned long size) int main(int argc, char **argv) { - unsigned long size = 4096; - char *buffer = xmalloc(size); + unsigned long size; + char *buffer; unsigned char result_sha1[20]; if (argc != 1) @@ -120,10 +120,8 @@ int main(int argc, char **argv) setup_git_directory(); - if (read_pipe(0, &buffer, &size)) { - free(buffer); + if (read_fd(0, &buffer, &size)) die("could not read from stdin"); - } /* Verify it for some basic sanity: it needs to start with "object <sha1>\ntype\ntagger " */ diff --git a/sha1_file.c b/sha1_file.c index 2b86086..91e8854 100644 --- a/sha1_file.c +++ b/sha1_file.c @@ -2286,18 +2286,17 @@ int has_sha1_file(const unsigned char *sha1) } /* - * reads from fd as long as possible into a supplied buffer of size bytes. - * If necessary the buffer's size is increased using realloc() + * reads from fd as long as possible and allocates a buffer to hold + * the contents. The buffer and size of the contents is returned in + * *return_buf and *return_size. In case of failure, the allocated + * buffers are freed, otherwise, the buffer must be freed using xfree. * * returns 0 if anything went fine and -1 otherwise - * - * NOTE: both buf and size may change, but even when -1 is returned - * you still have to free() it yourself. */ -int read_pipe(int fd, char** return_buf, unsigned long* return_size) +int read_fd(int fd, char** return_buf, unsigned long* return_size) { - char* buf = *return_buf; - unsigned long size = *return_size; + unsigned long size = 4096; + char* buf = xmalloc(size); ssize_t iret; unsigned long off = 0; @@ -2315,21 +2314,38 @@ int read_pipe(int fd, char** return_buf, unsigned long* return_size) *return_buf = buf; *return_size = off; - if (iret < 0) + if (iret < 0) { + free(buf); + return -1; + } + + return 0; +} + +int read_path(const char *path, char** return_buf, unsigned long* return_size) +{ + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + if (read_fd(fd, return_buf, return_size)) { + close(fd); return -1; + } + close(fd); + return 0; } int index_pipe(unsigned char *sha1, int fd, const char *type, int write_object) { - unsigned long size = 4096; - char *buf = xmalloc(size); + unsigned long size; + char *buf; int ret; - if (read_pipe(fd, &buf, &size)) { - free(buf); + if (read_fd(fd, &buf, &size)) return -1; - } if (!type) type = blob_type; diff --git a/t/t7800-commit.sh b/t/t7800-commit.sh new file mode 100644 index 0000000..1f97caa --- /dev/null +++ b/t/t7800-commit.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# +# Copyright (c) 2007 Kristian Høgsberg <krh@xxxxxxxxxx> +# + +# FIXME: Test the various index usages, -i and -o, test reflog, +# signoff, hooks + +test_description='git-commit' +. ./test-lib.sh + +# Pick a date so we get consistent commits. 7/7/07 means good luck! +export GIT_AUTHOR_DATE="July 7, 2007" +export GIT_COMMITTER_DATE="July 7, 2007" + +echo "bongo bongo" >file +test_expect_success \ + "initial status" \ + "git-add file && \ + git-status | grep 'Initial commit'" + +test_expect_failure \ + "fail initial amend" \ + "git-commit -m initial --amend" + +test_expect_success \ + "initial commit" \ + "git-commit -m initial" + +test_expect_failure \ + "testing nothing to commit" \ + "git-commit -m initial" + +echo "bongo bongo bongo" >file + +test_expect_success \ + "next commit" \ + "git-commit -m next -a" + +echo "more bongo: bongo bongo bongo bongo" >file + +test_expect_failure \ + "commit message from non-existing file" \ + "git-commit -F gah -a" + +cat >msg <<EOF + + + +Signed-off-by: hula +EOF +test_expect_failure \ + "empty commit message" \ + "git-commit -F msg -a" + +echo "this is the commit message, coming from a file" >msg +test_expect_success \ + "commit message from file" \ + "git-commit -F msg -a" + +cat >editor <<\EOF +#!/bin/sh +sed -i -e "s/a file/an amend commit/g" $1 +EOF +chmod 755 editor + +test_expect_success \ + "amend commit" \ + "VISUAL=./editor git-commit --amend" + +echo "enough with the bongos" >file +test_expect_failure \ + "passing --amend and -F" \ + "git-commit -F msg --amend ." + +test_expect_success \ + "using message from other commit" \ + "git-commit -C HEAD^ ." + +cat >editor <<\EOF +#!/bin/sh +sed -i -e "s/amend/older/g" $1 +EOF +chmod 755 editor + +echo "hula hula" >file +test_expect_success \ + "editing message from other commit" \ + "VISUAL=./editor git-commit -c HEAD^ -a" + +echo "silly new contents" >file +test_expect_success \ + "message from stdin" \ + "echo commit message from stdin | git-commit -F - -a" + +echo "gak" >file +test_expect_success \ + "overriding author from command line" \ + "git-commit -m 'author' --author 'Rubber Duck <rduck@xxxxxxxxxx>' -a" + +test_expect_success \ + "interactive add" \ + "echo 7 | git-commit --interactive | grep 'What now'" + +test_expect_success \ + "showing committed revisions" \ + "git-rev-list HEAD >current" + +# We could just check the head sha1, but checking each commit makes it +# easier to isolate bugs. + +cat >expected <<\EOF +9a7ad90c32f43aecc081722a72f26ead981bd6fa +d5c6c34f0361d64d3d1b1f26736b1ae98e2baa90 +ca0ddb06fc90f3f857dd623f8418804aad75d9fa +9cc5d9b9e5a29f1c46d0d0cf2dd716fb8241316a +ca750e97c137587aa181b6b9526b3b04fb9db667 +4515202a60be41cb1b8f6b89edb3f6948130ac1c +7cc9a125522fe28b84cd3f6c7aeef6e5c62b3f8b +EOF + +test_expect_success \ + 'validate git-rev-list output.' \ + 'diff current expected' + +test_done diff --git a/wt-status.c b/wt-status.c index 5205420..5bf800a 100644 --- a/wt-status.c +++ b/wt-status.c @@ -54,29 +54,30 @@ void wt_status_prepare(struct wt_status *s) s->reference = "HEAD"; } -static void wt_status_print_cached_header(const char *reference) +static void wt_status_print_cached_header(struct wt_status *s) { const char *c = color(WT_STATUS_HEADER); - color_printf_ln(c, "# Changes to be committed:"); - if (reference) { - color_printf_ln(c, "# (use \"git reset %s <file>...\" to unstage)", reference); + color_fprintf_ln(s->fp, c, "# Changes to be committed:"); + if (s->reference) { + color_fprintf_ln(s->fp, c, "# (use \"git reset %s <file>...\" to unstage)", s->reference); } else { - color_printf_ln(c, "# (use \"git rm --cached <file>...\" to unstage)"); + color_fprintf_ln(s->fp, c, "# (use \"git rm --cached <file>...\" to unstage)"); } - color_printf_ln(c, "#"); + color_fprintf_ln(s->fp, c, "#"); } -static void wt_status_print_header(const char *main, const char *sub) +static void wt_status_print_header(struct wt_status *s, + const char *main, const char *sub) { const char *c = color(WT_STATUS_HEADER); - color_printf_ln(c, "# %s:", main); - color_printf_ln(c, "# (%s)", sub); - color_printf_ln(c, "#"); + color_fprintf_ln(s->fp, c, "# %s:", main); + color_fprintf_ln(s->fp, c, "# (%s)", sub); + color_fprintf_ln(s->fp, c, "#"); } -static void wt_status_print_trailer(void) +static void wt_status_print_trailer(struct wt_status *s) { - color_printf_ln(color(WT_STATUS_HEADER), "#"); + color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#"); } static const char *quote_crlf(const char *in, char *buf, size_t sz) @@ -108,7 +109,8 @@ static const char *quote_crlf(const char *in, char *buf, size_t sz) return ret; } -static void wt_status_print_filepair(int t, struct diff_filepair *p) +static void wt_status_print_filepair(struct wt_status *s, + int t, struct diff_filepair *p) { const char *c = color(t); const char *one, *two; @@ -117,36 +119,36 @@ static void wt_status_print_filepair(int t, struct diff_filepair *p) one = quote_crlf(p->one->path, onebuf, sizeof(onebuf)); two = quote_crlf(p->two->path, twobuf, sizeof(twobuf)); - color_printf(color(WT_STATUS_HEADER), "#\t"); + color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t"); switch (p->status) { case DIFF_STATUS_ADDED: - color_printf(c, "new file: %s", one); + color_fprintf(s->fp, c, "new file: %s", one); break; case DIFF_STATUS_COPIED: - color_printf(c, "copied: %s -> %s", one, two); + color_fprintf(s->fp, c, "copied: %s -> %s", one, two); break; case DIFF_STATUS_DELETED: - color_printf(c, "deleted: %s", one); + color_fprintf(s->fp, c, "deleted: %s", one); break; case DIFF_STATUS_MODIFIED: - color_printf(c, "modified: %s", one); + color_fprintf(s->fp, c, "modified: %s", one); break; case DIFF_STATUS_RENAMED: - color_printf(c, "renamed: %s -> %s", one, two); + color_fprintf(s->fp, c, "renamed: %s -> %s", one, two); break; case DIFF_STATUS_TYPE_CHANGED: - color_printf(c, "typechange: %s", one); + color_fprintf(s->fp, c, "typechange: %s", one); break; case DIFF_STATUS_UNKNOWN: - color_printf(c, "unknown: %s", one); + color_fprintf(s->fp, c, "unknown: %s", one); break; case DIFF_STATUS_UNMERGED: - color_printf(c, "unmerged: %s", one); + color_fprintf(s->fp, c, "unmerged: %s", one); break; default: die("bug: unhandled diff status %c", p->status); } - printf("\n"); + fprintf(s->fp, "\n"); } static void wt_status_print_updated_cb(struct diff_queue_struct *q, @@ -160,14 +162,14 @@ static void wt_status_print_updated_cb(struct diff_queue_struct *q, if (q->queue[i]->status == 'U') continue; if (!shown_header) { - wt_status_print_cached_header(s->reference); + wt_status_print_cached_header(s); s->commitable = 1; shown_header = 1; } - wt_status_print_filepair(WT_STATUS_UPDATED, q->queue[i]); + wt_status_print_filepair(s, WT_STATUS_UPDATED, q->queue[i]); } if (shown_header) - wt_status_print_trailer(); + wt_status_print_trailer(s); } static void wt_status_print_changed_cb(struct diff_queue_struct *q, @@ -184,18 +186,18 @@ static void wt_status_print_changed_cb(struct diff_queue_struct *q, msg = use_add_rm_msg; break; } - wt_status_print_header("Changed but not updated", msg); + wt_status_print_header(s, "Changed but not updated", msg); } for (i = 0; i < q->nr; i++) - wt_status_print_filepair(WT_STATUS_CHANGED, q->queue[i]); + wt_status_print_filepair(s, WT_STATUS_CHANGED, q->queue[i]); if (q->nr) - wt_status_print_trailer(); + wt_status_print_trailer(s); } static void wt_read_cache(struct wt_status *s) { discard_cache(); - read_cache(); + read_cache_from(s->index_file); } static void wt_status_print_initial(struct wt_status *s) @@ -206,16 +208,16 @@ static void wt_status_print_initial(struct wt_status *s) wt_read_cache(s); if (active_nr) { s->commitable = 1; - wt_status_print_cached_header(NULL); + wt_status_print_cached_header(s); } for (i = 0; i < active_nr; i++) { - color_printf(color(WT_STATUS_HEADER), "#\t"); - color_printf_ln(color(WT_STATUS_UPDATED), "new file: %s", + color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t"); + color_fprintf_ln(s->fp, color(WT_STATUS_UPDATED), "new file: %s", quote_crlf(active_cache[i]->name, buf, sizeof(buf))); } if (active_nr) - wt_status_print_trailer(); + wt_status_print_trailer(s); } static void wt_status_print_updated(struct wt_status *s) @@ -281,12 +283,12 @@ static void wt_status_print_untracked(struct wt_status *s) } if (!shown_header) { s->workdir_untracked = 1; - wt_status_print_header("Untracked files", + wt_status_print_header(s, "Untracked files", use_add_to_include_msg); shown_header = 1; } - color_printf(color(WT_STATUS_HEADER), "#\t"); - color_printf_ln(color(WT_STATUS_UNTRACKED), "%.*s", + color_fprintf(s->fp, color(WT_STATUS_HEADER), "#\t"); + color_fprintf_ln(s->fp, color(WT_STATUS_UNTRACKED), "%.*s", ent->len, ent->name); } } @@ -316,14 +318,14 @@ void wt_status_print(struct wt_status *s) branch_name = ""; on_what = "Not currently on any branch."; } - color_printf_ln(color(WT_STATUS_HEADER), + color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "# %s%s", on_what, branch_name); } if (s->is_initial) { - color_printf_ln(color(WT_STATUS_HEADER), "#"); - color_printf_ln(color(WT_STATUS_HEADER), "# Initial commit"); - color_printf_ln(color(WT_STATUS_HEADER), "#"); + color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#"); + color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "# Initial commit"); + color_fprintf_ln(s->fp, color(WT_STATUS_HEADER), "#"); wt_status_print_initial(s); } else { @@ -337,7 +339,7 @@ void wt_status_print(struct wt_status *s) wt_status_print_verbose(s); if (!s->commitable) { if (s->amend) - printf("# No changes\n"); + fprintf(s->fp, "# No changes\n"); else if (s->workdir_dirty) printf("no changes added to commit (use \"git add\" and/or \"git commit -a\")\n"); else if (s->workdir_untracked) diff --git a/wt-status.h b/wt-status.h index cfea4ae..7744932 100644 --- a/wt-status.h +++ b/wt-status.h @@ -1,6 +1,8 @@ #ifndef STATUS_H #define STATUS_H +#include <stdio.h> + enum color_wt_status { WT_STATUS_HEADER, WT_STATUS_UPDATED, @@ -19,6 +21,8 @@ struct wt_status { int commitable; int workdir_dirty; int workdir_untracked; + const char *index_file; + FILE *fp; }; int git_status_config(const char *var, const char *value); -- 1.5.2.GIT - To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html