diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index e0e730b6c4..bb4abe26bb 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -9,7 +9,7 @@ SYNOPSIS -------- [verse] 'git-add' [-n] [-v] [-f] [--interactive | -i] [--patch | -p] [-u] [--refresh] - [--] ... + [--ignore-errors] [--] ... DESCRIPTION ----------- @@ -83,6 +83,11 @@ OPTIONS Don't add the file(s), but only refresh their stat() information in the index. +\--ignore-errors:: + If some files could not be added because of errors indexing + them, do not abort the operation, but continue adding the + others. The command shall still exit with non-zero status. + \--:: This option can be used to separate command-line options from the list of files, (useful when filenames might be mistaken diff --git a/Documentation/git-cvsexportcommit.txt b/Documentation/git-cvsexportcommit.txt index 9a47b4c397..363c36d694 100644 --- a/Documentation/git-cvsexportcommit.txt +++ b/Documentation/git-cvsexportcommit.txt @@ -65,11 +65,17 @@ OPTIONS -w:: Specify the location of the CVS checkout to use for the export. This option does not require GIT_DIR to be set before execution if the - current directory is within a git repository. + current directory is within a git repository. The default is the + value of 'cvsexportcommit.cvsdir'. -v:: Verbose. +CONFIGURATION +------------- +cvsexportcommit.cvsdir:: + The default location of the CVS checkout to use for the export. + EXAMPLES -------- diff --git a/Documentation/git-daemon.txt b/Documentation/git-daemon.txt index fd83bc7833..cf261dd40d 100644 --- a/Documentation/git-daemon.txt +++ b/Documentation/git-daemon.txt @@ -174,7 +174,7 @@ upload-pack:: upload-archive:: This serves `git-archive --remote`. It is disabled by default, but a repository can enable it by setting - `daemon.uploadarchive` configuration item to `true`. + `daemon.uploadarch` configuration item to `true`. receive-pack:: This serves `git-send-pack` clients, allowing anonymous @@ -257,7 +257,7 @@ selectively enable/disable services per repository:: ---------------------------------------------------------------- [daemon] uploadpack = false - uploadarchive = true + uploadarch = true ---------------------------------------------------------------- diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt index b6b2fe92a1..69599ffb67 100644 --- a/Documentation/git-rev-parse.txt +++ b/Documentation/git-rev-parse.txt @@ -378,6 +378,31 @@ C? option C with an optional argument" eval `echo "$OPTS_SPEC" | git-rev-parse --parseopt -- "$@" || echo exit $?` ------------ +EXAMPLES +-------- + +* Print the object name of the current commit: ++ +------------ +$ git rev-parse --verify HEAD +------------ + +* Print the commit object name from the revision in the $REV shell variable: ++ +------------ +$ git rev-parse --verify $REV +------------ ++ +This will error out if $REV is empty or not a valid revision. + +* Same as above: ++ +------------ +$ git rev-parse --default master --verify $REV +------------ ++ +but if $REV is empty, the commit object name from master will be printed. + Author ------ diff --git a/Documentation/git-svn.txt b/Documentation/git-svn.txt index c6b56b4ef3..3eae1ebb7d 100644 --- a/Documentation/git-svn.txt +++ b/Documentation/git-svn.txt @@ -196,10 +196,10 @@ Any other arguments are passed directly to `git log' independently of git-svn functions. 'create-ignore':: - Recursively finds the svn:ignore property on directories and creates matching .gitignore files. The resulting files are staged to - be committed, but are not committed. + be committed, but are not committed. Use -r/--revision to refer to a + specfic revision. 'show-ignore':: Recursively finds and lists the svn:ignore property on @@ -223,6 +223,19 @@ Any other arguments are passed directly to `git log' argument. Use the --url option to output only the value of the 'URL:' field. +'proplist':: + Lists the properties stored in the Subversion repository about a + given file or directory. Use -r/--revision to refer to a specific + Subversion revision. + +'propget':: + Gets the Subversion property given as the first argument, for a + file. A specific revision can be specified with -r/--revision. + +'show-externals':: + Shows the Subversion externals. Use -r/--revision to specify a + specific revision. + -- OPTIONS diff --git a/Documentation/rev-list-options.txt b/Documentation/rev-list-options.txt index 2648a55085..ce6a1017a3 100644 --- a/Documentation/rev-list-options.txt +++ b/Documentation/rev-list-options.txt @@ -75,6 +75,16 @@ you would get an output line this: -xxxxxxx... 1st on a ----------------------------------------------------------------------- +--graph:: + + Draw a text-based graphical representation of the commit history + on the left hand side of the output. This may cause extra lines + to be printed in between commits, in order for the graph history + to be drawn properly. ++ +This implies the '--topo-order' option by default, but the +'--date-order' option may also be specified. + Diff Formatting ~~~~~~~~~~~~~~~ diff --git a/Documentation/technical/api-history-graph.txt b/Documentation/technical/api-history-graph.txt new file mode 100644 index 0000000000..ce1c08ee86 --- /dev/null +++ b/Documentation/technical/api-history-graph.txt @@ -0,0 +1,179 @@ +history graph API +================= + +The graph API is used to draw a text-based representation of the commit +history. The API generates the graph in a line-by-line fashion. + +Functions +--------- + +Core functions: + +* `graph_init()` creates a new `struct git_graph` + +* `graph_release()` destroys a `struct git_graph`, and frees the memory + associated with it. + +* `graph_update()` moves the graph to a new commit. + +* `graph_next_line()` outputs the next line of the graph into a strbuf. It + does not add a terminating newline. + +* `graph_padding_line()` outputs a line of vertical padding in the graph. It + is similar to `graph_next_line()`, but is guaranteed to never print the line + containing the current commit. Where `graph_next_line()` would print the + commit line next, `graph_padding_line()` prints a line that simply extends + all branch lines downwards one row, leaving their positions unchanged. + +* `graph_is_commit_finished()` determines if the graph has output all lines + necessary for the current commit. If `graph_update()` is called before all + lines for the current commit have been printed, the next call to + `graph_next_line()` will output an ellipsis, to indicate that a portion of + the graph was omitted. + +The following utility functions are wrappers around `graph_next_line()` and +`graph_is_commit_finished()`. They always print the output to stdout. +They can all be called with a NULL graph argument, in which case no graph +output will be printed. + +* `graph_show_commit()` calls `graph_next_line()` until it returns non-zero. + This prints all graph lines up to, and including, the line containing this + commit. Output is printed to stdout. The last line printed does not contain + a terminating newline. This should not be called if the commit line has + already been printed, or it will loop forever. + +* `graph_show_oneline()` calls `graph_next_line()` and prints the result to + stdout. The line printed does not contain a terminating newline. + +* `graph_show_padding()` calls `graph_padding_line()` and prints the result to + stdout. The line printed does not contain a terminating newline. + +* `graph_show_remainder()` calls `graph_next_line()` until + `graph_is_commit_finished()` returns non-zero. Output is printed to stdout. + The last line printed does not contain a terminating newline. Returns 1 if + output was printed, and 0 if no output was necessary. + +* `graph_show_strbuf()` prints the specified strbuf to stdout, prefixing all + lines but the first with a graph line. The caller is responsible for + ensuring graph output for the first line has already been printed to stdout. + (This can be done with `graph_show_commit()` or `graph_show_oneline()`.) If + a NULL graph is supplied, the strbuf is printed as-is. + +* `graph_show_commit_msg()` is similar to `graph_show_strbuf()`, but it also + prints the remainder of the graph, if more lines are needed after the strbuf + ends. It is better than directly calling `graph_show_strbuf()` followed by + `graph_show_remainder()` since it properly handles buffers that do not end in + a terminating newline. The output printed by `graph_show_commit_msg()` will + end in a newline if and only if the strbuf ends in a newline. + +Data structure +-------------- +`struct git_graph` is an opaque data type used to store the current graph +state. + +Calling sequence +---------------- + +* Create a `struct git_graph` by calling `graph_init()`. When using the + revision walking API, this is done automatically by `setup_revisions()` if + the '--graph' option is supplied. + +* Use the revision walking API to walk through a group of contiguous commits. + The `get_revision()` function automatically calls `graph_update()` each time + it is invoked. + +* For each commit, call `graph_next_line()` repeatedly, until + `graph_is_commit_finished()` returns non-zero. Each call go + `graph_next_line()` will output a single line of the graph. The resulting + lines will not contain any newlines. `graph_next_line()` returns 1 if the + resulting line contains the current commit, or 0 if this is merely a line + needed to adjust the graph before or after the current commit. This return + value can be used to determine where to print the commit summary information + alongside the graph output. + +Limitations +----------- + +* `graph_update()` must be called with commits in topological order. It should + not be called on a commit if it has already been invoked with an ancestor of + that commit, or the graph output will be incorrect. + +* `graph_update()` must be called on a contiguous group of commits. If + `graph_update()` is called on a particular commit, it should later be called + on all parents of that commit. Parents must not be skipped, or the graph + output will appear incorrect. ++ +`graph_update()` may be used on a pruned set of commits only if the parent list +has been rewritten so as to include only ancestors from the pruned set. + +* The graph API does not currently support reverse commit ordering. In + order to implement reverse ordering, the graphing API needs an + (efficient) mechanism to find the children of a commit. + +Sample usage +------------ + +------------ +struct commit *commit; +struct git_graph *graph = graph_init(); + +while ((commit = get_revision(opts)) != NULL) { + graph_update(graph, commit); + while (!graph_is_commit_finished(graph)) + { + struct strbuf sb; + int is_commit_line; + + strbuf_init(&sb, 0); + is_commit_line = graph_next_line(graph, &sb); + fputs(sb.buf, stdout); + + if (is_commit_line) + log_tree_commit(opts, commit); + else + putchar(opts->diffopt.line_termination); + } +} + +graph_release(graph); +------------ + +Sample output +------------- + +The following is an example of the output from the graph API. This output does +not include any commit summary information--callers are responsible for +outputting that information, if desired. + +------------ +* +* +M +|\ +* | +| | * +| \ \ +| \ \ +M-. \ \ +|\ \ \ \ +| | * | | +| | | | | * +| | | | | * +| | | | | M +| | | | | |\ +| | | | | | * +| * | | | | | +| | | | | M \ +| | | | | |\ | +| | | | * | | | +| | | | * | | | +* | | | | | | | +| |/ / / / / / +|/| / / / / / +* | | | | | | +|/ / / / / / +* | | | | | +| | | | | * +| | | | |/ +| | | | * +------------ diff --git a/Makefile b/Makefile index 54fd4cc7b2..364f1d7463 100644 --- a/Makefile +++ b/Makefile @@ -345,6 +345,7 @@ LIB_H += diff.h LIB_H += dir.h LIB_H += fsck.h LIB_H += git-compat-util.h +LIB_H += graph.h LIB_H += grep.h LIB_H += hash.h LIB_H += list-objects.h @@ -410,6 +411,7 @@ LIB_OBJS += entry.o LIB_OBJS += environment.o LIB_OBJS += exec_cmd.o LIB_OBJS += fsck.o +LIB_OBJS += graph.o LIB_OBJS += grep.o LIB_OBJS += hash.o LIB_OBJS += help.o diff --git a/builtin-add.c b/builtin-add.c index 4a91e3eb11..73235ed08a 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -79,12 +79,18 @@ static void fill_directory(struct dir_struct *dir, const char **pathspec, prune_directory(dir, pathspec, baselen); } +struct update_callback_data +{ + int flags; + int add_errors; +}; + static void update_callback(struct diff_queue_struct *q, struct diff_options *opt, void *cbdata) { - int i, verbose; + int i; + struct update_callback_data *data = cbdata; - verbose = *((int *)cbdata); for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; const char *path = p->one->path; @@ -94,27 +100,35 @@ static void update_callback(struct diff_queue_struct *q, case DIFF_STATUS_UNMERGED: case DIFF_STATUS_MODIFIED: case DIFF_STATUS_TYPE_CHANGED: - add_file_to_cache(path, verbose); + if (add_file_to_cache(path, data->flags & ADD_FILES_VERBOSE)) { + if (!(data->flags & ADD_FILES_IGNORE_ERRORS)) + die("updating files failed"); + data->add_errors++; + } break; case DIFF_STATUS_DELETED: remove_file_from_cache(path); - if (verbose) + if (data->flags & ADD_FILES_VERBOSE) printf("remove '%s'\n", path); break; } } } -void add_files_to_cache(int verbose, const char *prefix, const char **pathspec) +int add_files_to_cache(const char *prefix, const char **pathspec, int flags) { + struct update_callback_data data; struct rev_info rev; init_revisions(&rev, prefix); setup_revisions(0, NULL, &rev, NULL); rev.prune_data = pathspec; rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; rev.diffopt.format_callback = update_callback; - rev.diffopt.format_callback_data = &verbose; + data.flags = flags; + data.add_errors = 0; + rev.diffopt.format_callback_data = &data; run_diff_files(&rev, DIFF_RACY_IS_MODIFIED); + return !!data.add_errors; } static void refresh(int verbose, const char **pathspec) @@ -177,6 +191,7 @@ static const char ignore_error[] = "The following paths are ignored by one of your .gitignore files:\n"; static int verbose = 0, show_only = 0, ignored_too = 0, refresh_only = 0; +static int ignore_add_errors; static struct option builtin_add_options[] = { OPT__DRY_RUN(&show_only), @@ -187,11 +202,22 @@ static struct option builtin_add_options[] = { OPT_BOOLEAN('f', NULL, &ignored_too, "allow adding otherwise ignored files"), OPT_BOOLEAN('u', NULL, &take_worktree_changes, "update tracked files"), OPT_BOOLEAN( 0 , "refresh", &refresh_only, "don't add, only refresh the index"), + OPT_BOOLEAN( 0 , "ignore-errors", &ignore_add_errors, "just skip files which cannot be added because of errors"), OPT_END(), }; +static int add_config(const char *var, const char *value) +{ + if (!strcasecmp(var, "add.ignore-errors")) { + ignore_add_errors = git_config_bool(var, value); + return 0; + } + return git_default_config(var, value); +} + int cmd_add(int argc, const char **argv, const char *prefix) { + int exit_status = 0; int i, newfd; const char **pathspec; struct dir_struct dir; @@ -203,16 +229,23 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (add_interactive) exit(interactive_add(argc, argv, prefix)); - git_config(git_default_config); + git_config(add_config); newfd = hold_locked_index(&lock_file, 1); if (take_worktree_changes) { + int flags = 0; const char **pathspec; if (read_cache() < 0) die("index file corrupt"); pathspec = get_pathspec(prefix, argv); - add_files_to_cache(verbose, prefix, pathspec); + + if (verbose) + flags |= ADD_FILES_VERBOSE; + if (ignore_add_errors) + flags |= ADD_FILES_IGNORE_ERRORS; + + exit_status = add_files_to_cache(prefix, pathspec, flags); goto finish; } @@ -254,7 +287,11 @@ int cmd_add(int argc, const char **argv, const char *prefix) } for (i = 0; i < dir.nr; i++) - add_file_to_cache(dir.entries[i]->name, verbose); + if (add_file_to_cache(dir.entries[i]->name, verbose)) { + if (!ignore_add_errors) + die("adding files failed"); + exit_status = 1; + } finish: if (active_cache_changed) { @@ -263,5 +300,5 @@ int cmd_add(int argc, const char **argv, const char *prefix) die("Unable to write new index file"); } - return 0; + return exit_status; } diff --git a/builtin-checkout.c b/builtin-checkout.c index 10ec137cce..05c06421b6 100644 --- a/builtin-checkout.c +++ b/builtin-checkout.c @@ -282,7 +282,7 @@ static int merge_working_tree(struct checkout_opts *opts, * entries in the index. */ - add_files_to_cache(0, NULL, NULL); + add_files_to_cache(NULL, NULL, 0); work = write_tree_from_memory(); ret = reset_to_new(new->commit->tree, opts->quiet); diff --git a/builtin-commit.c b/builtin-commit.c index 0baec6db6a..d75224381b 100644 --- a/builtin-commit.c +++ b/builtin-commit.c @@ -179,9 +179,10 @@ static void add_remove_files(struct path_list *list) struct stat st; struct path_list_item *p = &(list->items[i]); - if (!lstat(p->path, &st)) - add_to_cache(p->path, &st, 0); - else + if (!lstat(p->path, &st)) { + if (add_to_cache(p->path, &st, 0)) + die("updating files failed"); + } else remove_file_from_cache(p->path); } } @@ -246,7 +247,7 @@ static char *prepare_index(int argc, const char **argv, const char *prefix) */ if (all || (also && pathspec && *pathspec)) { int fd = hold_locked_index(&index_lock, 1); - add_files_to_cache(0, also ? prefix : NULL, pathspec); + add_files_to_cache(also ? prefix : NULL, pathspec, 0); refresh_cache(REFRESH_QUIET); if (write_cache(fd, active_cache, active_nr) || close_lock_file(&index_lock)) diff --git a/builtin-mv.c b/builtin-mv.c index 94f6dd2aad..fb8ffb41aa 100644 --- a/builtin-mv.c +++ b/builtin-mv.c @@ -256,7 +256,8 @@ int cmd_mv(int argc, const char **argv, const char *prefix) for (i = 0; i < added.nr; i++) { const char *path = added.items[i].path; - add_file_to_cache(path, verbose); + if (add_file_to_cache(path, verbose)) + die("updating index entries failed"); } for (i = 0; i < deleted.nr; i++) diff --git a/builtin-pack-objects.c b/builtin-pack-objects.c index 777f272668..e20851e1c9 100644 --- a/builtin-pack-objects.c +++ b/builtin-pack-objects.c @@ -43,6 +43,7 @@ struct object_entry { */ void *delta_data; /* cached delta (uncompressed) */ unsigned long delta_size; /* delta data size (uncompressed) */ + unsigned long z_delta_size; /* delta data size (compressed) */ unsigned int hash; /* name hint hash */ enum object_type type; enum object_type in_pack_type; /* could be delta */ @@ -65,7 +66,8 @@ static struct pack_idx_entry **written_list; static uint32_t nr_objects, nr_alloc, nr_result, nr_written; static int non_empty; -static int no_reuse_delta, no_reuse_object, keep_unreachable, include_tag; +static int reuse_delta = 1, reuse_object = 1; +static int keep_unreachable, include_tag; static int local; static int incremental; static int allow_ofs_delta; @@ -102,24 +104,53 @@ static uint32_t written, written_delta; static uint32_t reused, reused_delta; -static void *delta_against(void *buf, unsigned long size, struct object_entry *entry) +static void *get_delta(struct object_entry *entry) { - unsigned long othersize, delta_size; + unsigned long size, base_size, delta_size; + void *buf, *base_buf, *delta_buf; enum object_type type; - void *otherbuf = read_sha1_file(entry->delta->idx.sha1, &type, &othersize); - void *delta_buf; - if (!otherbuf) + buf = read_sha1_file(entry->idx.sha1, &type, &size); + if (!buf) + die("unable to read %s", sha1_to_hex(entry->idx.sha1)); + base_buf = read_sha1_file(entry->delta->idx.sha1, &type, &base_size); + if (!base_buf) die("unable to read %s", sha1_to_hex(entry->delta->idx.sha1)); - delta_buf = diff_delta(otherbuf, othersize, + delta_buf = diff_delta(base_buf, base_size, buf, size, &delta_size, 0); - if (!delta_buf || delta_size != entry->delta_size) + if (!delta_buf || delta_size != entry->delta_size) die("delta size changed"); - free(buf); - free(otherbuf); + free(buf); + free(base_buf); return delta_buf; } +static unsigned long do_compress(void **pptr, unsigned long size) +{ + z_stream stream; + void *in, *out; + unsigned long maxsize; + + memset(&stream, 0, sizeof(stream)); + deflateInit(&stream, pack_compression_level); + maxsize = deflateBound(&stream, size); + + in = *pptr; + out = xmalloc(maxsize); + *pptr = out; + + stream.next_in = in; + stream.avail_in = size; + stream.next_out = out; + stream.avail_out = maxsize; + while (deflate(&stream, Z_FINISH) == Z_OK) + ; /* nothing */ + deflateEnd(&stream); + + free(in); + return stream.total_out; +} + /* * The per-object header is a pretty dense thing, which is * - first byte: low four bits are "size", then three bits of "type", @@ -222,42 +253,42 @@ static unsigned long write_object(struct sha1file *f, struct object_entry *entry, off_t write_offset) { - unsigned long size; - enum object_type type; + unsigned long size, limit, datalen; void *buf; - unsigned char header[10]; - unsigned char dheader[10]; + unsigned char header[10], dheader[10]; unsigned hdrlen; - off_t datalen; - enum object_type obj_type; - int to_reuse = 0; - /* write limit if limited packsize and not first object */ - unsigned long limit = pack_size_limit && nr_written ? - pack_size_limit - write_offset : 0; - /* no if no delta */ - int usable_delta = !entry->delta ? 0 : - /* yes if unlimited packfile */ - !pack_size_limit ? 1 : - /* no if base written to previous pack */ - entry->delta->idx.offset == (off_t)-1 ? 0 : - /* otherwise double-check written to this - * pack, like we do below - */ - entry->delta->idx.offset ? 1 : 0; + enum object_type type; + int usable_delta, to_reuse; if (!pack_to_stdout) crc32_begin(f); - obj_type = entry->type; - if (no_reuse_object) + type = entry->type; + + /* write limit if limited packsize and not first object */ + limit = pack_size_limit && nr_written ? + pack_size_limit - write_offset : 0; + + if (!entry->delta) + usable_delta = 0; /* no delta */ + else if (!pack_size_limit) + usable_delta = 1; /* unlimited packfile */ + else if (entry->delta->idx.offset == (off_t)-1) + usable_delta = 0; /* base was written to another pack */ + else if (entry->delta->idx.offset) + usable_delta = 1; /* base already exists in this pack */ + else + usable_delta = 0; /* base could end up in another pack */ + + if (!reuse_object) to_reuse = 0; /* explicit */ else if (!entry->in_pack) to_reuse = 0; /* can't reuse what we don't have */ - else if (obj_type == OBJ_REF_DELTA || obj_type == OBJ_OFS_DELTA) + else if (type == OBJ_REF_DELTA || type == OBJ_OFS_DELTA) /* check_object() decided it for us ... */ to_reuse = usable_delta; /* ... but pack split may override that */ - else if (obj_type != entry->in_pack_type) + else if (type != entry->in_pack_type) to_reuse = 0; /* pack has delta which is unusable */ else if (entry->delta) to_reuse = 0; /* we want to pack afresh */ @@ -267,50 +298,42 @@ static unsigned long write_object(struct sha1file *f, */ if (!to_reuse) { - z_stream stream; - unsigned long maxsize; - void *out; if (!usable_delta) { - buf = read_sha1_file(entry->idx.sha1, &obj_type, &size); + buf = read_sha1_file(entry->idx.sha1, &type, &size); if (!buf) die("unable to read %s", sha1_to_hex(entry->idx.sha1)); + /* + * make sure no cached delta data remains from a + * previous attempt before a pack split occured. + */ + free(entry->delta_data); + entry->delta_data = NULL; + entry->z_delta_size = 0; } else if (entry->delta_data) { size = entry->delta_size; buf = entry->delta_data; entry->delta_data = NULL; - obj_type = (allow_ofs_delta && entry->delta->idx.offset) ? + type = (allow_ofs_delta && entry->delta->idx.offset) ? OBJ_OFS_DELTA : OBJ_REF_DELTA; } else { - buf = read_sha1_file(entry->idx.sha1, &type, &size); - if (!buf) - die("unable to read %s", sha1_to_hex(entry->idx.sha1)); - buf = delta_against(buf, size, entry); + buf = get_delta(entry); size = entry->delta_size; - obj_type = (allow_ofs_delta && entry->delta->idx.offset) ? + type = (allow_ofs_delta && entry->delta->idx.offset) ? OBJ_OFS_DELTA : OBJ_REF_DELTA; } - /* compress the data to store and put compressed length in datalen */ - memset(&stream, 0, sizeof(stream)); - deflateInit(&stream, pack_compression_level); - maxsize = deflateBound(&stream, size); - out = xmalloc(maxsize); - /* Compress it */ - stream.next_in = buf; - stream.avail_in = size; - stream.next_out = out; - stream.avail_out = maxsize; - while (deflate(&stream, Z_FINISH) == Z_OK) - /* nothing */; - deflateEnd(&stream); - datalen = stream.total_out; + + if (entry->z_delta_size) + datalen = entry->z_delta_size; + else + datalen = do_compress(&buf, size); /* * The object header is a byte of 'type' followed by zero or * more bytes of length. */ - hdrlen = encode_header(obj_type, size, header); + hdrlen = encode_header(type, size, header); - if (obj_type == OBJ_OFS_DELTA) { + if (type == OBJ_OFS_DELTA) { /* * Deltas with relative base contain an additional * encoding of the relative offset for the delta @@ -322,20 +345,18 @@ static unsigned long write_object(struct sha1file *f, while (ofs >>= 7) dheader[--pos] = 128 | (--ofs & 127); if (limit && hdrlen + sizeof(dheader) - pos + datalen + 20 >= limit) { - free(out); free(buf); return 0; } sha1write(f, header, hdrlen); sha1write(f, dheader + pos, sizeof(dheader) - pos); hdrlen += sizeof(dheader) - pos; - } else if (obj_type == OBJ_REF_DELTA) { + } else if (type == OBJ_REF_DELTA) { /* * Deltas with a base reference contain * an additional 20 bytes for the base sha1. */ if (limit && hdrlen + 20 + datalen + 20 >= limit) { - free(out); free(buf); return 0; } @@ -344,14 +365,12 @@ static unsigned long write_object(struct sha1file *f, hdrlen += 20; } else { if (limit && hdrlen + datalen + 20 >= limit) { - free(out); free(buf); return 0; } sha1write(f, header, hdrlen); } - sha1write(f, out, datalen); - free(out); + sha1write(f, buf, datalen); free(buf); } else { @@ -361,11 +380,11 @@ static unsigned long write_object(struct sha1file *f, off_t offset; if (entry->delta) { - obj_type = (allow_ofs_delta && entry->delta->idx.offset) ? + type = (allow_ofs_delta && entry->delta->idx.offset) ? OBJ_OFS_DELTA : OBJ_REF_DELTA; reused_delta++; } - hdrlen = encode_header(obj_type, entry->size, header); + hdrlen = encode_header(type, entry->size, header); offset = entry->in_pack_offset; revidx = find_pack_revindex(p, offset); datalen = revidx[1].offset - offset; @@ -374,7 +393,7 @@ static unsigned long write_object(struct sha1file *f, die("bad packed object CRC for %s", sha1_to_hex(entry->idx.sha1)); offset += entry->in_pack_header_size; datalen -= entry->in_pack_header_size; - if (obj_type == OBJ_OFS_DELTA) { + if (type == OBJ_OFS_DELTA) { off_t ofs = entry->idx.offset - entry->delta->idx.offset; unsigned pos = sizeof(dheader) - 1; dheader[pos] = ofs & 127; @@ -385,7 +404,7 @@ static unsigned long write_object(struct sha1file *f, sha1write(f, header, hdrlen); sha1write(f, dheader + pos, sizeof(dheader) - pos); hdrlen += sizeof(dheader) - pos; - } else if (obj_type == OBJ_REF_DELTA) { + } else if (type == OBJ_REF_DELTA) { if (limit && hdrlen + 20 + datalen + 20 >= limit) return 0; sha1write(f, header, hdrlen); @@ -452,11 +471,10 @@ static void write_pack_file(void) struct sha1file *f; off_t offset, offset_one, last_obj_offset = 0; struct pack_header hdr; - int do_progress = progress >> pack_to_stdout; uint32_t nr_remaining = nr_result; time_t last_mtime = 0; - if (do_progress) + if (progress > pack_to_stdout) progress_state = start_progress("Writing objects", nr_result); written_list = xmalloc(nr_objects * sizeof(*written_list)); @@ -1022,7 +1040,7 @@ static void check_object(struct object_entry *entry) unuse_pack(&w_curs); return; case OBJ_REF_DELTA: - if (!no_reuse_delta && !entry->preferred_base) + if (reuse_delta && !entry->preferred_base) base_ref = use_pack(p, &w_curs, entry->in_pack_offset + used, NULL); entry->in_pack_header_size = used + 20; @@ -1045,7 +1063,7 @@ static void check_object(struct object_entry *entry) die("delta base offset out of bound for %s", sha1_to_hex(entry->idx.sha1)); ofs = entry->in_pack_offset - ofs; - if (!no_reuse_delta && !entry->preferred_base) { + if (reuse_delta && !entry->preferred_base) { struct revindex_entry *revidx; revidx = find_pack_revindex(p, ofs); base_ref = nth_packed_object_sha1(p, revidx->nr); @@ -1233,7 +1251,7 @@ static int try_delta(struct unpacked *trg, struct unpacked *src, * We do not bother to try a delta that we discarded * on an earlier try, but only when reusing delta data. */ - if (!no_reuse_delta && trg_entry->in_pack && + if (reuse_delta && trg_entry->in_pack && trg_entry->in_pack == src_entry->in_pack && trg_entry->in_pack_type != OBJ_REF_DELTA && trg_entry->in_pack_type != OBJ_OFS_DELTA) @@ -1441,11 +1459,34 @@ static void find_deltas(struct object_entry **list, unsigned *list_size, best_base = other_idx; } + /* + * If we decided to cache the delta data, then it is best + * to compress it right away. First because we have to do + * it anyway, and doing it here while we're threaded will + * save a lot of time in the non threaded write phase, + * as well as allow for caching more deltas within + * the same cache size limit. + * ... + * But only if not writing to stdout, since in that case + * the network is most likely throttling writes anyway, + * and therefore it is best to go to the write phase ASAP + * instead, as we can afford spending more time compressing + * between writes at that moment. + */ + if (entry->delta_data && !pack_to_stdout) { + entry->z_delta_size = do_compress(&entry->delta_data, + entry->delta_size); + cache_lock(); + delta_cache_size -= entry->delta_size; + delta_cache_size += entry->z_delta_size; + cache_unlock(); + } + /* if we made n a delta, and if n is already at max * depth, leaving it in the window is pointless. we * should evict it first. */ - if (entry->delta && depth <= n->depth) + if (entry->delta && max_depth <= n->depth) continue; /* @@ -1688,7 +1729,7 @@ static void prepare_pack(int window, int depth) if (entry->delta) /* This happens if we decided to reuse existing - * delta from a pack. "!no_reuse_delta &&" is implied. + * delta from a pack. "reuse_delta &&" is implied. */ continue; @@ -2050,11 +2091,11 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) continue; } if (!strcmp("--no-reuse-delta", arg)) { - no_reuse_delta = 1; + reuse_delta = 0; continue; } if (!strcmp("--no-reuse-object", arg)) { - no_reuse_object = no_reuse_delta = 1; + reuse_object = reuse_delta = 0; continue; } if (!strcmp("--delta-base-offset", arg)) { diff --git a/builtin-rev-list.c b/builtin-rev-list.c index edc0bd35bb..54d55cc3a3 100644 --- a/builtin-rev-list.c +++ b/builtin-rev-list.c @@ -10,6 +10,7 @@ #include "list-objects.h" #include "builtin.h" #include "log-tree.h" +#include "graph.h" /* bits #0-15 in revision.h */ @@ -58,6 +59,8 @@ static const char *header_prefix; static void finish_commit(struct commit *commit); static void show_commit(struct commit *commit) { + graph_show_commit(revs.graph); + if (show_timestamp) printf("%lu ", commit->date); if (header_prefix) @@ -77,7 +80,7 @@ static void show_commit(struct commit *commit) stdout); else fputs(sha1_to_hex(commit->object.sha1), stdout); - if (revs.parents) { + if (revs.print_parents) { struct commit_list *parents = commit->parents; while (parents) { printf(" %s", sha1_to_hex(parents->item->object.sha1)); @@ -96,9 +99,48 @@ static void show_commit(struct commit *commit) pretty_print_commit(revs.commit_format, commit, &buf, revs.abbrev, NULL, NULL, revs.date_mode, 0); - if (buf.len) - printf("%s%c", buf.buf, hdr_termination); + if (revs.graph) { + if (buf.len) { + if (revs.commit_format != CMIT_FMT_ONELINE) + graph_show_oneline(revs.graph); + + graph_show_commit_msg(revs.graph, &buf); + + /* + * Add a newline after the commit message. + * + * Usually, this newline produces a blank + * padding line between entries, in which case + * we need to add graph padding on this line. + * + * However, the commit message may not end in a + * newline. In this case the newline simply + * ends the last line of the commit message, + * and we don't need any graph output. (This + * always happens with CMIT_FMT_ONELINE, and it + * happens with CMIT_FMT_USERFORMAT when the + * format doesn't explicitly end in a newline.) + */ + if (buf.len && buf.buf[buf.len - 1] == '\n') + graph_show_padding(revs.graph); + putchar('\n'); + } else { + /* + * If the message buffer is empty, just show + * the rest of the graph output for this + * commit. + */ + if (graph_show_remainder(revs.graph)) + putchar('\n'); + } + } else { + if (buf.len) + printf("%s%c", buf.buf, hdr_termination); + } strbuf_release(&buf); + } else { + if (graph_show_remainder(revs.graph)) + putchar('\n'); } maybe_flush_or_die(stdout, "stdout"); finish_commit(commit); diff --git a/cache.h b/cache.h index 6a21371b0e..bea2364c55 100644 --- a/cache.h +++ b/cache.h @@ -782,7 +782,13 @@ extern int convert_to_git(const char *path, const char *src, size_t len, extern int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst); /* add */ -void add_files_to_cache(int verbose, const char *prefix, const char **pathspec); +#define ADD_FILES_VERBOSE 01 +#define ADD_FILES_IGNORE_ERRORS 02 +/* + * return 0 if success, 1 - if addition of a file failed and + * ADD_FILES_IGNORE_ERRORS was specified in flags + */ +int add_files_to_cache(const char *prefix, const char **pathspec, int flags); /* diff.c */ extern int diff_auto_refresh_index; diff --git a/git-am.sh b/git-am.sh index 75886a8f2f..b48096ec20 100755 --- a/git-am.sh +++ b/git-am.sh @@ -11,7 +11,7 @@ git-am [options] --skip -- d,dotest= (removed -- do not use) i,interactive run interactively -b,binary pass --allo-binary-replacement to git-apply +b,binary pass --allow-binary-replacement to git-apply 3,3way allow fall back on 3way merging if needed s,signoff add a Signed-off-by line to the commit message u,utf8 recode into utf8 (default) diff --git a/git-cvsexportcommit.perl b/git-cvsexportcommit.perl index b6036bd4d3..c93bd9c9b5 100755 --- a/git-cvsexportcommit.perl +++ b/git-cvsexportcommit.perl @@ -6,6 +6,7 @@ use File::Temp qw(tempdir); use Data::Dumper; use File::Basename qw(basename dirname); use File::Spec; +use Git; our ($opt_h, $opt_P, $opt_p, $opt_v, $opt_c, $opt_f, $opt_a, $opt_m, $opt_d, $opt_u, $opt_w); @@ -15,6 +16,10 @@ $opt_h && usage(); die "Need at least one commit identifier!" unless @ARGV; +# Get git-config settings +my $repo = Git->repository(); +$opt_w = $repo->config('cvsexportcommit.cvsdir') unless defined $opt_w; + if ($opt_w) { # Remember where GIT_DIR is before changing to CVS checkout unless ($ENV{GIT_DIR}) { diff --git a/git-merge.sh b/git-merge.sh index 69b35d87e6..5fc5f5201f 100755 --- a/git-merge.sh +++ b/git-merge.sh @@ -9,11 +9,9 @@ git-merge [options] ... git-merge [options] HEAD -- stat show a diffstat at the end of the merge -n,no-stat don't show a diffstat at the end of the merge +n don't show a diffstat at the end of the merge summary (synonym to --stat) -no-summary (synonym to --no-stat) log add list of one-line log to merge commit message -no-log don't add list of one-line log to merge commit message squash create a single commit instead of doing a merge commit perform a commit if the merge sucesses (default) ff allow fast forward (default) diff --git a/git-send-email.perl b/git-send-email.perl index 1e1d98656d..a598fdc890 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -521,8 +521,30 @@ EOT open(C,"<",$compose_filename) or die "Failed to open $compose_filename : " . $!; + my $need_8bit_cte = file_has_nonascii($compose_filename); + my $in_body = 0; while() { next if m/^GIT: /; + if (!$in_body && /^\n$/) { + $in_body = 1; + if ($need_8bit_cte) { + print C2 "MIME-Version: 1.0\n", + "Content-Type: text/plain; ", + "charset=utf-8\n", + "Content-Transfer-Encoding: 8bit\n"; + } + } + if (!$in_body && /^MIME-Version:/i) { + $need_8bit_cte = 0; + } + if (!$in_body && /^Subject: ?(.*)/i) { + my $subject = $1; + $_ = "Subject: " . + ($subject =~ /[^[:ascii:]]/ ? + quote_rfc2047($subject) : + $subject) . + "\n"; + } print C2 $_; } close(C); @@ -613,6 +635,14 @@ sub unquote_rfc2047 { return wantarray ? ($_, $encoding) : $_; } +sub quote_rfc2047 { + local $_ = shift; + my $encoding = shift || 'utf-8'; + s/([^-a-zA-Z0-9!*+\/])/sprintf("=%02X", ord($1))/eg; + s/(.*)/=\?$encoding\?q\?$1\?=/; + return $_; +} + # use the simplest quoting being able to handle the recipient sub sanitize_address { @@ -630,13 +660,12 @@ sub sanitize_address # rfc2047 is needed if a non-ascii char is included if ($recipient_name =~ /[^[:ascii:]]/) { - $recipient_name =~ s/([^-a-zA-Z0-9!*+\/])/sprintf("=%02X", ord($1))/eg; - $recipient_name =~ s/(.*)/=\?utf-8\?q\?$1\?=/; + $recipient_name = quote_rfc2047($recipient_name); } # double quotes are needed if specials or CTLs are included elsif ($recipient_name =~ /[][()<>@,;:\\".\000-\037\177]/) { - $recipient_name =~ s/(["\\\r])/\\$1/; + $recipient_name =~ s/(["\\\r])/\\$1/g; $recipient_name = "\"$recipient_name\""; } @@ -959,3 +988,13 @@ sub validate_patch { } return undef; } + +sub file_has_nonascii { + my $fn = shift; + open(my $fh, '<', $fn) + or die "unable to open $fn: $!\n"; + while (my $line = <$fh>) { + return 1 if $line =~ /[^[:ascii:]]/; + } + return 0; +} diff --git a/graph.c b/graph.c new file mode 100644 index 0000000000..9d6ed30b0b --- /dev/null +++ b/graph.c @@ -0,0 +1,945 @@ +#include "cache.h" +#include "commit.h" +#include "graph.h" +#include "diff.h" +#include "revision.h" + +/* + * TODO: + * - Add colors to the graph. + * Pick a color for each column, and print all characters + * in that column with the specified color. + * + * - Limit the number of columns, similar to the way gitk does. + * If we reach more than a specified number of columns, omit + * sections of some columns. + * + * - The output during the GRAPH_PRE_COMMIT and GRAPH_COLLAPSING states + * could be made more compact by printing horizontal lines, instead of + * long diagonal lines. For example, during collapsing, something like + * this: instead of this: + * | | | | | | | | | | + * | |_|_|/ | | | |/ + * |/| | | | | |/| + * | | | | | |/| | + * |/| | | + * | | | | + * + * If there are several parallel diagonal lines, they will need to be + * replaced with horizontal lines on subsequent rows. + */ + +struct column { + /* + * The parent commit of this column. + */ + struct commit *commit; + /* + * XXX: Once we add support for colors, struct column could also + * contain the color of its branch line. + */ +}; + +enum graph_state { + GRAPH_PADDING, + GRAPH_SKIP, + GRAPH_PRE_COMMIT, + GRAPH_COMMIT, + GRAPH_POST_MERGE, + GRAPH_COLLAPSING +}; + +struct git_graph { + /* + * The commit currently being processed + */ + struct commit *commit; + /* + * The number of parents this commit has. + * (Stored so we don't have to walk over them each time we need + * this number) + */ + int num_parents; + /* + * The width of the graph output for this commit. + * All rows for this commit are padded to this width, so that + * messages printed after the graph output are aligned. + */ + int width; + /* + * The next expansion row to print + * when state is GRAPH_PRE_COMMIT + */ + int expansion_row; + /* + * The current output state. + * This tells us what kind of line graph_next_line() should output. + */ + enum graph_state state; + /* + * The maximum number of columns that can be stored in the columns + * and new_columns arrays. This is also half the number of entries + * that can be stored in the mapping and new_mapping arrays. + */ + int column_capacity; + /* + * The number of columns (also called "branch lines" in some places) + */ + int num_columns; + /* + * The number of columns in the new_columns array + */ + int num_new_columns; + /* + * The number of entries in the mapping array + */ + int mapping_size; + /* + * The column state before we output the current commit. + */ + struct column *columns; + /* + * The new column state after we output the current commit. + * Only valid when state is GRAPH_COLLAPSING. + */ + struct column *new_columns; + /* + * An array that tracks the current state of each + * character in the output line during state GRAPH_COLLAPSING. + * Each entry is -1 if this character is empty, or a non-negative + * integer if the character contains a branch line. The value of + * the integer indicates the target position for this branch line. + * (I.e., this array maps the current column positions to their + * desired positions.) + * + * The maximum capacity of this array is always + * sizeof(int) * 2 * column_capacity. + */ + int *mapping; + /* + * A temporary array for computing the next mapping state + * while we are outputting a mapping line. This is stored as part + * of the git_graph simply so we don't have to allocate a new + * temporary array each time we have to output a collapsing line. + */ + int *new_mapping; +}; + +struct git_graph *graph_init(void) +{ + struct git_graph *graph = xmalloc(sizeof(struct git_graph)); + graph->commit = NULL; + graph->num_parents = 0; + graph->expansion_row = 0; + graph->state = GRAPH_PADDING; + graph->num_columns = 0; + graph->num_new_columns = 0; + graph->mapping_size = 0; + + /* + * Allocate a reasonably large default number of columns + * We'll automatically grow columns later if we need more room. + */ + graph->column_capacity = 30; + graph->columns = xmalloc(sizeof(struct column) * + graph->column_capacity); + graph->new_columns = xmalloc(sizeof(struct column) * + graph->column_capacity); + graph->mapping = xmalloc(sizeof(int) * 2 * graph->column_capacity); + graph->new_mapping = xmalloc(sizeof(int) * 2 * graph->column_capacity); + + return graph; +} + +void graph_release(struct git_graph *graph) +{ + free(graph->columns); + free(graph->new_columns); + free(graph->mapping); + free(graph); +} + +static void graph_ensure_capacity(struct git_graph *graph, int num_columns) +{ + if (graph->column_capacity >= num_columns) + return; + + do { + graph->column_capacity *= 2; + } while (graph->column_capacity < num_columns); + + graph->columns = xrealloc(graph->columns, + sizeof(struct column) * + graph->column_capacity); + graph->new_columns = xrealloc(graph->new_columns, + sizeof(struct column) * + graph->column_capacity); + graph->mapping = xrealloc(graph->mapping, + sizeof(int) * 2 * graph->column_capacity); + graph->new_mapping = xrealloc(graph->new_mapping, + sizeof(int) * 2 * graph->column_capacity); +} + +static void graph_insert_into_new_columns(struct git_graph *graph, + struct commit *commit, + int *mapping_index) +{ + int i; + + /* + * Ignore uinteresting and pruned commits + */ + if (commit->object.flags & (UNINTERESTING | TREESAME)) + return; + + /* + * If the commit is already in the new_columns list, we don't need to + * add it. Just update the mapping correctly. + */ + for (i = 0; i < graph->num_new_columns; i++) { + if (graph->new_columns[i].commit == commit) { + graph->mapping[*mapping_index] = i; + *mapping_index += 2; + return; + } + } + + /* + * This commit isn't already in new_columns. Add it. + */ + graph->new_columns[graph->num_new_columns].commit = commit; + graph->mapping[*mapping_index] = graph->num_new_columns; + *mapping_index += 2; + graph->num_new_columns++; +} + +static void graph_update_width(struct git_graph *graph, + int is_commit_in_existing_columns) +{ + /* + * Compute the width needed to display the graph for this commit. + * This is the maximum width needed for any row. All other rows + * will be padded to this width. + * + * Compute the number of columns in the widest row: + * Count each existing column (graph->num_columns), and each new + * column added by this commit. + */ + int max_cols = graph->num_columns + graph->num_parents; + + /* + * Even if the current commit has no parents, it still takes up a + * column for itself. + */ + if (graph->num_parents < 1) + max_cols++; + + /* + * We added a column for the the current commit as part of + * graph->num_parents. If the current commit was already in + * graph->columns, then we have double counted it. + */ + if (is_commit_in_existing_columns) + max_cols--; + + /* + * Each column takes up 2 spaces + */ + graph->width = max_cols * 2; +} + +static void graph_update_columns(struct git_graph *graph) +{ + struct commit_list *parent; + struct column *tmp_columns; + int max_new_columns; + int mapping_idx; + int i, seen_this, is_commit_in_columns; + + /* + * Swap graph->columns with graph->new_columns + * graph->columns contains the state for the previous commit, + * and new_columns now contains the state for our commit. + * + * We'll re-use the old columns array as storage to compute the new + * columns list for the commit after this one. + */ + tmp_columns = graph->columns; + graph->columns = graph->new_columns; + graph->num_columns = graph->num_new_columns; + + graph->new_columns = tmp_columns; + graph->num_new_columns = 0; + + /* + * Now update new_columns and mapping with the information for the + * commit after this one. + * + * First, make sure we have enough room. At most, there will + * be graph->num_columns + graph->num_parents columns for the next + * commit. + */ + max_new_columns = graph->num_columns + graph->num_parents; + graph_ensure_capacity(graph, max_new_columns); + + /* + * Clear out graph->mapping + */ + graph->mapping_size = 2 * max_new_columns; + for (i = 0; i < graph->mapping_size; i++) + graph->mapping[i] = -1; + + /* + * Populate graph->new_columns and graph->mapping + * + * Some of the parents of this commit may already be in + * graph->columns. If so, graph->new_columns should only contain a + * single entry for each such commit. graph->mapping should + * contain information about where each current branch line is + * supposed to end up after the collapsing is performed. + */ + seen_this = 0; + mapping_idx = 0; + is_commit_in_columns = 1; + for (i = 0; i <= graph->num_columns; i++) { + struct commit *col_commit; + if (i == graph->num_columns) { + if (seen_this) + break; + is_commit_in_columns = 0; + col_commit = graph->commit; + } else { + col_commit = graph->columns[i].commit; + } + + if (col_commit == graph->commit) { + seen_this = 1; + for (parent = graph->commit->parents; + parent; + parent = parent->next) { + graph_insert_into_new_columns(graph, + parent->item, + &mapping_idx); + } + } else { + graph_insert_into_new_columns(graph, col_commit, + &mapping_idx); + } + } + + /* + * Shrink mapping_size to be the minimum necessary + */ + while (graph->mapping_size > 1 && + graph->mapping[graph->mapping_size - 1] < 0) + graph->mapping_size--; + + /* + * Compute graph->width for this commit + */ + graph_update_width(graph, is_commit_in_columns); +} + +void graph_update(struct git_graph *graph, struct commit *commit) +{ + struct commit_list *parent; + + /* + * Set the new commit + */ + graph->commit = commit; + + /* + * Count how many parents this commit has + */ + graph->num_parents = 0; + for (parent = commit->parents; parent; parent = parent->next) + graph->num_parents++; + + /* + * Call graph_update_columns() to update + * columns, new_columns, and mapping. + */ + graph_update_columns(graph); + + graph->expansion_row = 0; + + /* + * Update graph->state. + * + * If the previous commit didn't get to the GRAPH_PADDING state, + * it never finished its output. Goto GRAPH_SKIP, to print out + * a line to indicate that portion of the graph is missing. + * + * Otherwise, if there are 3 or more parents, we need to print + * extra rows before the commit, to expand the branch lines around + * it and make room for it. + * + * If there are less than 3 parents, we can immediately print the + * commit line. + */ + if (graph->state != GRAPH_PADDING) + graph->state = GRAPH_SKIP; + else if (graph->num_parents >= 3) + graph->state = GRAPH_PRE_COMMIT; + else + graph->state = GRAPH_COMMIT; +} + +static int graph_is_mapping_correct(struct git_graph *graph) +{ + int i; + + /* + * The mapping is up to date if each entry is at its target, + * or is 1 greater than its target. + * (If it is 1 greater than the target, '/' will be printed, so it + * will look correct on the next row.) + */ + for (i = 0; i < graph->mapping_size; i++) { + int target = graph->mapping[i]; + if (target < 0) + continue; + if (target == (i / 2)) + continue; + return 0; + } + + return 1; +} + +static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb) +{ + /* + * Add additional spaces to the end of the strbuf, so that all + * lines for a particular commit have the same width. + * + * This way, fields printed to the right of the graph will remain + * aligned for the entire commit. + */ + int extra; + if (sb->len >= graph->width) + return; + + extra = graph->width - sb->len; + strbuf_addf(sb, "%*s", (int) extra, ""); +} + +static void graph_output_padding_line(struct git_graph *graph, + struct strbuf *sb) +{ + int i; + + /* + * We could conceivable be called with a NULL commit + * if our caller has a bug, and invokes graph_next_line() + * immediately after graph_init(), without first calling + * graph_update(). Return without outputting anything in this + * case. + */ + if (!graph->commit) + return; + + /* + * Output a padding row, that leaves all branch lines unchanged + */ + for (i = 0; i < graph->num_new_columns; i++) { + strbuf_addstr(sb, "| "); + } + + graph_pad_horizontally(graph, sb); +} + +static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb) +{ + /* + * Output an ellipsis to indicate that a portion + * of the graph is missing. + */ + strbuf_addstr(sb, "..."); + graph_pad_horizontally(graph, sb); + + if (graph->num_parents >= 3) + graph->state = GRAPH_PRE_COMMIT; + else + graph->state = GRAPH_COMMIT; +} + +static void graph_output_pre_commit_line(struct git_graph *graph, + struct strbuf *sb) +{ + int num_expansion_rows; + int i, seen_this; + + /* + * This function formats a row that increases the space around a commit + * with multiple parents, to make room for it. It should only be + * called when there are 3 or more parents. + * + * We need 2 extra rows for every parent over 2. + */ + assert(graph->num_parents >= 3); + num_expansion_rows = (graph->num_parents - 2) * 2; + + /* + * graph->expansion_row tracks the current expansion row we are on. + * It should be in the range [0, num_expansion_rows - 1] + */ + assert(0 <= graph->expansion_row && + graph->expansion_row < num_expansion_rows); + + /* + * Output the row + */ + seen_this = 0; + for (i = 0; i < graph->num_columns; i++) { + struct column *col = &graph->columns[i]; + if (col->commit == graph->commit) { + seen_this = 1; + strbuf_addf(sb, "| %*s", graph->expansion_row, ""); + } else if (seen_this) { + strbuf_addstr(sb, "\\ "); + } else { + strbuf_addstr(sb, "| "); + } + } + + graph_pad_horizontally(graph, sb); + + /* + * Increment graph->expansion_row, + * and move to state GRAPH_COMMIT if necessary + */ + graph->expansion_row++; + if (graph->expansion_row >= num_expansion_rows) + graph->state = GRAPH_COMMIT; +} + +void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb) +{ + int seen_this = 0; + int i, j; + + /* + * Output the row containing this commit + * Iterate up to and including graph->num_columns, + * since the current commit may not be in any of the existing + * columns. (This happens when the current commit doesn't have any + * children that we have already processed.) + */ + seen_this = 0; + for (i = 0; i <= graph->num_columns; i++) { + struct commit *col_commit; + if (i == graph->num_columns) { + if (seen_this) + break; + col_commit = graph->commit; + } else { + col_commit = graph->columns[i].commit; + } + + if (col_commit == graph->commit) { + seen_this = 1; + if (graph->num_parents > 1) + strbuf_addch(sb, 'M'); + else + strbuf_addch(sb, '*'); + + if (graph->num_parents < 2) + strbuf_addch(sb, ' '); + else if (graph->num_parents == 2) + strbuf_addstr(sb, " "); + else { + int num_dashes = + ((graph->num_parents - 2) * 2) - 1; + for (j = 0; j < num_dashes; j++) + strbuf_addch(sb, '-'); + strbuf_addstr(sb, ". "); + } + } else if (seen_this && (graph->num_parents > 1)) { + strbuf_addstr(sb, "\\ "); + } else { + strbuf_addstr(sb, "| "); + } + } + + graph_pad_horizontally(graph, sb); + + /* + * Update graph->state + */ + if (graph->num_parents > 1) + graph->state = GRAPH_POST_MERGE; + else if (graph_is_mapping_correct(graph)) + graph->state = GRAPH_PADDING; + else + graph->state = GRAPH_COLLAPSING; +} + +void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb) +{ + int seen_this = 0; + int i, j; + + /* + * Output the post-merge row + */ + for (i = 0; i <= graph->num_columns; i++) { + struct commit *col_commit; + if (i == graph->num_columns) { + if (seen_this) + break; + col_commit = graph->commit; + } else { + col_commit = graph->columns[i].commit; + } + + if (col_commit == graph->commit) { + seen_this = 1; + strbuf_addch(sb, '|'); + for (j = 0; j < graph->num_parents - 1; j++) + strbuf_addstr(sb, "\\ "); + if (graph->num_parents == 2) + strbuf_addch(sb, ' '); + } else if (seen_this && (graph->num_parents > 2)) { + strbuf_addstr(sb, "\\ "); + } else { + strbuf_addstr(sb, "| "); + } + } + + graph_pad_horizontally(graph, sb); + + /* + * Update graph->state + */ + if (graph_is_mapping_correct(graph)) + graph->state = GRAPH_PADDING; + else + graph->state = GRAPH_COLLAPSING; +} + +void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb) +{ + int i; + int *tmp_mapping; + + /* + * Clear out the new_mapping array + */ + for (i = 0; i < graph->mapping_size; i++) + graph->new_mapping[i] = -1; + + for (i = 0; i < graph->mapping_size; i++) { + int target = graph->mapping[i]; + if (target < 0) + continue; + + /* + * Since update_columns() always inserts the leftmost + * column first, each branch's target location should + * always be either its current location or to the left of + * its current location. + * + * We never have to move branches to the right. This makes + * the graph much more legible, since whenever branches + * cross, only one is moving directions. + */ + assert(target * 2 <= i); + + if (target * 2 == i) { + /* + * This column is already in the + * correct place + */ + assert(graph->new_mapping[i] == -1); + graph->new_mapping[i] = target; + } else if (graph->new_mapping[i - 1] < 0) { + /* + * Nothing is to the left. + * Move to the left by one + */ + graph->new_mapping[i - 1] = target; + } else if (graph->new_mapping[i - 1] == target) { + /* + * There is a branch line to our left + * already, and it is our target. We + * combine with this line, since we share + * the same parent commit. + * + * We don't have to add anything to the + * output or new_mapping, since the + * existing branch line has already taken + * care of it. + */ + } else { + /* + * There is a branch line to our left, + * but it isn't our target. We need to + * cross over it. + * + * The space just to the left of this + * branch should always be empty. + */ + assert(graph->new_mapping[i - 1] > target); + assert(graph->new_mapping[i - 2] < 0); + graph->new_mapping[i - 2] = target; + } + } + + /* + * The new mapping may be 1 smaller than the old mapping + */ + if (graph->new_mapping[graph->mapping_size - 1] < 0) + graph->mapping_size--; + + /* + * Output out a line based on the new mapping info + */ + for (i = 0; i < graph->mapping_size; i++) { + int target = graph->new_mapping[i]; + if (target < 0) + strbuf_addch(sb, ' '); + else if (target * 2 == i) + strbuf_addch(sb, '|'); + else + strbuf_addch(sb, '/'); + } + + graph_pad_horizontally(graph, sb); + + /* + * Swap mapping and new_mapping + */ + tmp_mapping = graph->mapping; + graph->mapping = graph->new_mapping; + graph->new_mapping = tmp_mapping; + + /* + * If graph->mapping indicates that all of the branch lines + * are already in the correct positions, we are done. + * Otherwise, we need to collapse some branch lines together. + */ + if (graph_is_mapping_correct(graph)) + graph->state = GRAPH_PADDING; +} + +int graph_next_line(struct git_graph *graph, struct strbuf *sb) +{ + switch (graph->state) { + case GRAPH_PADDING: + graph_output_padding_line(graph, sb); + return 0; + case GRAPH_SKIP: + graph_output_skip_line(graph, sb); + return 0; + case GRAPH_PRE_COMMIT: + graph_output_pre_commit_line(graph, sb); + return 0; + case GRAPH_COMMIT: + graph_output_commit_line(graph, sb); + return 1; + case GRAPH_POST_MERGE: + graph_output_post_merge_line(graph, sb); + return 0; + case GRAPH_COLLAPSING: + graph_output_collapsing_line(graph, sb); + return 0; + } + + assert(0); + return 0; +} + +void graph_padding_line(struct git_graph *graph, struct strbuf *sb) +{ + int i, j; + + if (graph->state != GRAPH_COMMIT) { + graph_next_line(graph, sb); + return; + } + + /* + * Output the row containing this commit + * Iterate up to and including graph->num_columns, + * since the current commit may not be in any of the existing + * columns. (This happens when the current commit doesn't have any + * children that we have already processed.) + */ + for (i = 0; i < graph->num_columns; i++) { + struct commit *col_commit = graph->columns[i].commit; + if (col_commit == graph->commit) { + strbuf_addch(sb, '|'); + + if (graph->num_parents < 3) + strbuf_addch(sb, ' '); + else { + int num_spaces = ((graph->num_parents - 2) * 2); + for (j = 0; j < num_spaces; j++) + strbuf_addch(sb, ' '); + } + } else { + strbuf_addstr(sb, "| "); + } + } + + graph_pad_horizontally(graph, sb); +} + +int graph_is_commit_finished(struct git_graph const *graph) +{ + return (graph->state == GRAPH_PADDING); +} + +void graph_show_commit(struct git_graph *graph) +{ + struct strbuf msgbuf; + int shown_commit_line = 0; + + if (!graph) + return; + + strbuf_init(&msgbuf, 0); + + while (!shown_commit_line) { + shown_commit_line = graph_next_line(graph, &msgbuf); + fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout); + if (!shown_commit_line) + putchar('\n'); + strbuf_setlen(&msgbuf, 0); + } + + strbuf_release(&msgbuf); +} + +void graph_show_oneline(struct git_graph *graph) +{ + struct strbuf msgbuf; + + if (!graph) + return; + + strbuf_init(&msgbuf, 0); + graph_next_line(graph, &msgbuf); + fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout); + strbuf_release(&msgbuf); +} + +void graph_show_padding(struct git_graph *graph) +{ + struct strbuf msgbuf; + + if (!graph) + return; + + strbuf_init(&msgbuf, 0); + graph_padding_line(graph, &msgbuf); + fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout); + strbuf_release(&msgbuf); +} + +int graph_show_remainder(struct git_graph *graph) +{ + struct strbuf msgbuf; + int shown = 0; + + if (!graph) + return 0; + + if (graph_is_commit_finished(graph)) + return 0; + + strbuf_init(&msgbuf, 0); + for (;;) { + graph_next_line(graph, &msgbuf); + fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout); + strbuf_setlen(&msgbuf, 0); + shown = 1; + + if (!graph_is_commit_finished(graph)) + putchar('\n'); + else + break; + } + strbuf_release(&msgbuf); + + return shown; +} + + +void graph_show_strbuf(struct git_graph *graph, struct strbuf const *sb) +{ + char *p; + + if (!graph) { + fwrite(sb->buf, sizeof(char), sb->len, stdout); + return; + } + + /* + * Print the strbuf line by line, + * and display the graph info before each line but the first. + */ + p = sb->buf; + while (p) { + size_t len; + char *next_p = strchr(p, '\n'); + if (next_p) { + next_p++; + len = next_p - p; + } else { + len = (sb->buf + sb->len) - p; + } + fwrite(p, sizeof(char), len, stdout); + if (next_p && *next_p != '\0') + graph_show_oneline(graph); + p = next_p; + } +} + +void graph_show_commit_msg(struct git_graph *graph, + struct strbuf const *sb) +{ + int newline_terminated; + + if (!graph) { + /* + * If there's no graph, just print the message buffer. + * + * The message buffer for CMIT_FMT_ONELINE and + * CMIT_FMT_USERFORMAT are already missing a terminating + * newline. All of the other formats should have it. + */ + fwrite(sb->buf, sizeof(char), sb->len, stdout); + return; + } + + newline_terminated = (sb->len && sb->buf[sb->len - 1] == '\n'); + + /* + * Show the commit message + */ + graph_show_strbuf(graph, sb); + + /* + * If there is more output needed for this commit, show it now + */ + if (!graph_is_commit_finished(graph)) { + /* + * If sb doesn't have a terminating newline, print one now, + * so we can start the remainder of the graph output on a + * new line. + */ + if (!newline_terminated) + putchar('\n'); + + graph_show_remainder(graph); + + /* + * If sb ends with a newline, our output should too. + */ + if (newline_terminated) + putchar('\n'); + } +} diff --git a/graph.h b/graph.h new file mode 100644 index 0000000000..a7748a5b22 --- /dev/null +++ b/graph.h @@ -0,0 +1,121 @@ +#ifndef GRAPH_H +#define GRAPH_H + +/* A graph is a pointer to this opaque structure */ +struct git_graph; + +/* + * Create a new struct git_graph. + * The graph should be freed with graph_release() when no longer needed. + */ +struct git_graph *graph_init(); + +/* + * Destroy a struct git_graph and free associated memory. + */ +void graph_release(struct git_graph *graph); + +/* + * Update a git_graph with a new commit. + * This will cause the graph to begin outputting lines for the new commit + * the next time graph_next_line() is called. + * + * If graph_update() is called before graph_is_commit_finished() returns 1, + * the next call to graph_next_line() will output an ellipsis ("...") + * to indicate that a portion of the graph is missing. + */ +void graph_update(struct git_graph *graph, struct commit *commit); + +/* + * Output the next line for a graph. + * This formats the next graph line into the specified strbuf. It is not + * terminated with a newline. + * + * Returns 1 if the line includes the current commit, and 0 otherwise. + * graph_next_line() will return 1 exactly once for each time + * graph_update() is called. + */ +int graph_next_line(struct git_graph *graph, struct strbuf *sb); + +/* + * Output a padding line in the graph. + * This is similar to graph_next_line(). However, it is guaranteed to + * never print the current commit line. Instead, if the commit line is + * next, it will simply output a line of vertical padding, extending the + * branch lines downwards, but leaving them otherwise unchanged. + */ +void graph_padding_line(struct git_graph *graph, struct strbuf *sb); + +/* + * Determine if a graph has finished outputting lines for the current + * commit. + * + * Returns 1 if graph_next_line() needs to be called again before + * graph_update() should be called. Returns 0 if no more lines are needed + * for this commit. If 0 is returned, graph_next_line() may still be + * called without calling graph_update(), and it will merely output + * appropriate "vertical padding" in the graph. + */ +int graph_is_commit_finished(struct git_graph const *graph); + + +/* + * graph_show_*: helper functions for printing to stdout + */ + + +/* + * If the graph is non-NULL, print the history graph to stdout, + * up to and including the line containing this commit. + * Does not print a terminating newline on the last line. + */ +void graph_show_commit(struct git_graph *graph); + +/* + * If the graph is non-NULL, print one line of the history graph to stdout. + * Does not print a terminating newline on the last line. + */ +void graph_show_oneline(struct git_graph *graph); + +/* + * If the graph is non-NULL, print one line of vertical graph padding to + * stdout. Does not print a terminating newline on the last line. + */ +void graph_show_padding(struct git_graph *graph); + +/* + * If the graph is non-NULL, print the rest of the history graph for this + * commit to stdout. Does not print a terminating newline on the last line. + */ +int graph_show_remainder(struct git_graph *graph); + +/* + * Print a strbuf to stdout. If the graph is non-NULL, all lines but the + * first will be prefixed with the graph output. + * + * If the strbuf ends with a newline, the output will end after this + * newline. A new graph line will not be printed after the final newline. + * If the strbuf is empty, no output will be printed. + * + * Since the first line will not include the graph ouput, the caller is + * responsible for printing this line's graph (perhaps via + * graph_show_commit() or graph_show_oneline()) before calling + * graph_show_strbuf(). + */ +void graph_show_strbuf(struct git_graph *graph, struct strbuf const *sb); + +/* + * Print a commit message strbuf and the remainder of the graph to stdout. + * + * This is similar to graph_show_strbuf(), but it always prints the + * remainder of the graph. + * + * If the strbuf ends with a newline, the output printed by + * graph_show_commit_msg() will end with a newline. If the strbuf is + * missing a terminating newline (including if it is empty), the output + * printed by graph_show_commit_msg() will also be missing a terminating + * newline. + */ +void graph_show_commit_msg(struct git_graph *graph, struct strbuf const *sb); + +#endif /* GRAPH_H */ diff --git a/log-tree.c b/log-tree.c index d3fb0e520c..1474d1f5d9 100644 --- a/log-tree.c +++ b/log-tree.c @@ -1,6 +1,7 @@ #include "cache.h" #include "diff.h" #include "commit.h" +#include "graph.h" #include "log-tree.h" #include "reflog-walk.h" @@ -165,11 +166,16 @@ void log_write_email_headers(struct rev_info *opt, const char *name, } printf("From %s Mon Sep 17 00:00:00 2001\n", name); - if (opt->message_id) + graph_show_oneline(opt->graph); + if (opt->message_id) { printf("Message-Id: <%s>\n", opt->message_id); - if (opt->ref_message_id) + graph_show_oneline(opt->graph); + } + if (opt->ref_message_id) { printf("In-Reply-To: <%s>\nReferences: <%s>\n", opt->ref_message_id, opt->ref_message_id); + graph_show_oneline(opt->graph); + } if (opt->mime_boundary) { static char subject_buffer[1024]; static char buffer[1024]; @@ -220,6 +226,8 @@ void show_log(struct rev_info *opt) opt->loginfo = NULL; if (!opt->verbose_header) { + graph_show_commit(opt->graph); + if (commit->object.flags & BOUNDARY) putchar('-'); else if (commit->object.flags & UNINTERESTING) @@ -231,9 +239,13 @@ void show_log(struct rev_info *opt) putchar('>'); } fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), stdout); - if (opt->parents) + if (opt->print_parents) show_parents(commit, abbrev_commit); show_decorations(commit); + if (opt->graph && !graph_is_commit_finished(opt->graph)) { + putchar('\n'); + graph_show_remainder(opt->graph); + } putchar(opt->diffopt.line_termination); return; } @@ -243,10 +255,32 @@ void show_log(struct rev_info *opt) * Otherwise, add a diffopt.line_termination character before all * entries but the first. (IOW, as a separator between entries) */ - if (opt->shown_one && !opt->use_terminator) + if (opt->shown_one && !opt->use_terminator) { + /* + * If entries are separated by a newline, the output + * should look human-readable. If the last entry ended + * with a newline, print the graph output before this + * newline. Otherwise it will end up as a completely blank + * line and will look like a gap in the graph. + * + * If the entry separator is not a newline, the output is + * primarily intended for programmatic consumption, and we + * never want the extra graph output before the entry + * separator. + */ + if (opt->diffopt.line_termination == '\n' && + !opt->missing_newline) + graph_show_padding(opt->graph); putchar(opt->diffopt.line_termination); + } opt->shown_one = 1; + /* + * If the history graph was requested, + * print the graph, up to this commit's line + */ + graph_show_commit(opt->graph); + /* * Print header line of header.. */ @@ -271,7 +305,7 @@ void show_log(struct rev_info *opt) } fputs(diff_unique_abbrev(commit->object.sha1, abbrev_commit), stdout); - if (opt->parents) + if (opt->print_parents) show_parents(commit, abbrev_commit); if (parent) printf(" (from %s)", @@ -279,8 +313,19 @@ void show_log(struct rev_info *opt) abbrev_commit)); show_decorations(commit); printf("%s", diff_get_color_opt(&opt->diffopt, DIFF_RESET)); - putchar(opt->commit_format == CMIT_FMT_ONELINE ? ' ' : '\n'); + if (opt->commit_format == CMIT_FMT_ONELINE) { + putchar(' '); + } else { + putchar('\n'); + graph_show_oneline(opt->graph); + } if (opt->reflog_info) { + /* + * setup_revisions() ensures that opt->reflog_info + * and opt->graph cannot both be set, + * so we don't need to worry about printing the + * graph info here. + */ show_reflog_message(opt->reflog_info, opt->commit_format == CMIT_FMT_ONELINE, opt->date_mode); @@ -304,13 +349,30 @@ void show_log(struct rev_info *opt) if (opt->add_signoff) append_signoff(&msgbuf, opt->add_signoff); - if (opt->show_log_size) + if (opt->show_log_size) { printf("log size %i\n", (int)msgbuf.len); + graph_show_oneline(opt->graph); + } - if (msgbuf.len) + /* + * Set opt->missing_newline if msgbuf doesn't + * end in a newline (including if it is empty) + */ + if (!msgbuf.len || msgbuf.buf[msgbuf.len - 1] != '\n') + opt->missing_newline = 1; + else + opt->missing_newline = 0; + + if (opt->graph) + graph_show_commit_msg(opt->graph, &msgbuf); + else fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout); - if (opt->use_terminator) + if (opt->use_terminator) { + if (!opt->missing_newline) + graph_show_padding(opt->graph); putchar('\n'); + } + strbuf_release(&msgbuf); } diff --git a/read-cache.c b/read-cache.c index 0382804e76..8b467f8f41 100644 --- a/read-cache.c +++ b/read-cache.c @@ -470,7 +470,7 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, unsigned ce_option = CE_MATCH_IGNORE_VALID|CE_MATCH_RACY_IS_DIRTY; if (!S_ISREG(st_mode) && !S_ISLNK(st_mode) && !S_ISDIR(st_mode)) - die("%s: can only add regular files, symbolic links or git-directories", path); + return error("%s: can only add regular files, symbolic links or git-directories", path); namelen = strlen(path); if (S_ISDIR(st_mode)) { @@ -505,12 +505,12 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, return 0; } if (index_path(ce->sha1, path, st, 1)) - die("unable to index file %s", path); + return error("unable to index file %s", path); if (ignore_case && alias && different_name(ce, alias)) ce = create_alias_ce(ce, alias); ce->ce_flags |= CE_ADDED; if (add_index_entry(istate, ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE)) - die("unable to add %s to index",path); + return error("unable to add %s to index",path); if (verbose) printf("add '%s'\n", path); return 0; diff --git a/revision.c b/revision.c index 4231ea2cce..7142cf96cd 100644 --- a/revision.c +++ b/revision.c @@ -6,6 +6,7 @@ #include "diff.h" #include "refs.h" #include "revision.h" +#include "graph.h" #include "grep.h" #include "reflog-walk.h" #include "patch-ids.h" @@ -415,7 +416,6 @@ static int add_parents_to_list(struct rev_info *revs, struct commit *commit, str { struct commit_list *parent = commit->parents; unsigned left_flag; - int add, rest; if (commit->object.flags & ADDED) return 0; @@ -462,19 +462,18 @@ static int add_parents_to_list(struct rev_info *revs, struct commit *commit, str left_flag = (commit->object.flags & SYMMETRIC_LEFT); - rest = !revs->first_parent_only; - for (parent = commit->parents, add = 1; parent; add = rest) { + for (parent = commit->parents; parent; parent = parent->next) { struct commit *p = parent->item; - parent = parent->next; if (parse_commit(p) < 0) return -1; p->object.flags |= left_flag; - if (p->object.flags & SEEN) - continue; - p->object.flags |= SEEN; - if (add) + if (!(p->object.flags & SEEN)) { + p->object.flags |= SEEN; insert_by_date(p, list); + } + if(revs->first_parent_only) + break; } return 0; } @@ -1105,7 +1104,8 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch } } if (!strcmp(arg, "--parents")) { - revs->parents = 1; + revs->rewrite_parents = 1; + revs->print_parents = 1; continue; } if (!strcmp(arg, "--dense")) { @@ -1202,6 +1202,12 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch get_commit_format(arg+8, revs); continue; } + if (!prefixcmp(arg, "--graph")) { + revs->topo_order = 1; + revs->rewrite_parents = 1; + revs->graph = graph_init(); + continue; + } if (!strcmp(arg, "--root")) { revs->show_root_diff = 1; continue; @@ -1396,6 +1402,15 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, const ch if (revs->reverse && revs->reflog_info) die("cannot combine --reverse with --walk-reflogs"); + /* + * Limitations on the graph functionality + */ + if (revs->reverse && revs->graph) + die("cannot combine --reverse with --graph"); + + if (revs->reflog_info && revs->graph) + die("cannot combine --walk-reflogs with --graph"); + return left; } @@ -1524,13 +1539,13 @@ enum commit_action simplify_commit(struct rev_info *revs, struct commit *commit) /* Commit without changes? */ if (commit->object.flags & TREESAME) { /* drop merges unless we want parenthood */ - if (!revs->parents) + if (!revs->rewrite_parents) return commit_ignore; /* non-merge - always ignore it */ if (!commit->parents || !commit->parents->next) return commit_ignore; } - if (revs->parents && rewrite_parents(revs, commit) < 0) + if (revs->rewrite_parents && rewrite_parents(revs, commit) < 0) return commit_error; } return commit_show; @@ -1597,7 +1612,7 @@ static void gc_boundary(struct object_array *array) } } -struct commit *get_revision(struct rev_info *revs) +static struct commit *get_revision_internal(struct rev_info *revs) { struct commit *c = NULL; struct commit_list *l; @@ -1704,3 +1719,11 @@ struct commit *get_revision(struct rev_info *revs) return c; } + +struct commit *get_revision(struct rev_info *revs) +{ + struct commit *c = get_revision_internal(revs); + if (c && revs->graph) + graph_update(revs->graph, c); + return c; +} diff --git a/revision.h b/revision.h index 31217f8c67..abce5001f1 100644 --- a/revision.h +++ b/revision.h @@ -46,7 +46,8 @@ struct rev_info { unpacked:1, /* see also ignore_packed below */ boundary:2, left_right:1, - parents:1, + rewrite_parents:1, + print_parents:1, reverse:1, cherry_pick:1, first_parent_only:1; @@ -65,7 +66,8 @@ struct rev_info { /* Format info */ unsigned int shown_one:1, abbrev_commit:1, - use_terminator:1; + use_terminator:1, + missing_newline:1; enum date_mode date_mode; const char **ignore_packed; /* pretend objects in these are unpacked */ @@ -88,6 +90,9 @@ struct rev_info { /* Filter by commit log message */ struct grep_opt *grep_filter; + /* Display history graph */ + struct git_graph *graph; + /* special limits */ int skip_count; int max_count; diff --git a/t/Makefile b/t/Makefile index 72d7884232..c6a60ab165 100644 --- a/t/Makefile +++ b/t/Makefile @@ -20,7 +20,7 @@ $(T): @echo "*** $@ ***"; GIT_CONFIG=.git/config '$(SHELL_PATH_SQ)' $@ $(GIT_TEST_OPTS) clean: - $(RM) -r trash + $(RM) -r 'trash directory' # we can test NO_OPTIMIZE_COMMITS independently of LC_ALL full-svn-test: diff --git a/t/README b/t/README index 73ed11bfe2..70841a4645 100644 --- a/t/README +++ b/t/README @@ -123,7 +123,7 @@ This test harness library does the following things: (or -h), it shows the test_description and exits. - Creates an empty test directory with an empty .git/objects - database and chdir(2) into it. This directory is 't/trash' + database and chdir(2) into it. This directory is 't/trash directory' if you must know, but I do not think you care. - Defines standard test helper functions for your scripts to diff --git a/t/t3700-add.sh b/t/t3700-add.sh index 206850615a..79cb00d1e8 100755 --- a/t/t3700-add.sh +++ b/t/t3700-add.sh @@ -183,4 +183,47 @@ test_expect_success 'git add --refresh' ' test -z "`git diff-index HEAD -- foo`" ' +test_expect_success 'git add should fail atomically upon an unreadable file' ' + git reset --hard && + date >foo1 && + date >foo2 && + chmod 0 foo2 && + test_must_fail git add --verbose . && + ! ( git ls-files foo1 | grep foo1 ) +' + +rm -f foo2 + +test_expect_success 'git add --ignore-errors' ' + git reset --hard && + date >foo1 && + date >foo2 && + chmod 0 foo2 && + test_must_fail git add --verbose --ignore-errors . && + git ls-files foo1 | grep foo1 +' + +rm -f foo2 + +test_expect_success 'git add (add.ignore-errors)' ' + git config add.ignore-errors 1 && + git reset --hard && + date >foo1 && + date >foo2 && + chmod 0 foo2 && + test_must_fail git add --verbose . && + git ls-files foo1 | grep foo1 +' +rm -f foo2 + +test_expect_success 'git add (add.ignore-errors = false)' ' + git config add.ignore-errors 0 && + git reset --hard && + date >foo1 && + date >foo2 && + chmod 0 foo2 && + test_must_fail git add --verbose . && + ! ( git ls-files foo1 | grep foo1 ) +' + test_done diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index f15be93e77..fae64eae9f 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -66,6 +66,11 @@ test_expect_success 'revert works (commit)' ' grep "unchanged *+3/-0 file" output ' +if test "$(git config --bool core.filemode)" = false +then + say 'skipping filemode tests (filesystem does not properly support modes)' +else + test_expect_success 'patch does not affect mode' ' git reset --hard && echo content >>file && @@ -84,5 +89,7 @@ test_expect_success 'stage mode but not hunk' ' git diff file | grep "+content" ' +fi +# end of tests disabled when filemode is not usable test_done diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 04baa61c04..3e4eb63f1c 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -233,4 +233,63 @@ test_expect_success 'sendemail.cc unset' ' test_cmp expected-show-all-headers actual-show-all-headers ' +test_expect_success '--compose adds MIME for utf8 body' ' + clean_fake_sendmail && + (echo "#!/bin/sh" && + echo "echo utf8 body: àéìöú >>\"\$1\"" + ) >fake-editor-utf8 && + chmod +x fake-editor-utf8 && + echo y | \ + GIT_EDITOR="\"$(pwd)/fake-editor-utf8\"" \ + GIT_SEND_EMAIL_NOTTY=1 \ + git send-email \ + --compose --subject foo \ + --from="Example " \ + --to=nobody@example.com \ + --smtp-server="$(pwd)/fake.sendmail" \ + $patches && + grep "^utf8 body" msgtxt1 && + grep "^Content-Type: text/plain; charset=utf-8" msgtxt1 +' + +test_expect_success '--compose respects user mime type' ' + clean_fake_sendmail && + (echo "#!/bin/sh" && + echo "(echo MIME-Version: 1.0" + echo " echo Content-Type: text/plain\\; charset=iso-8859-1" + echo " echo Content-Transfer-Encoding: 8bit" + echo " echo Subject: foo" + echo " echo " + echo " echo utf8 body: àéìöú) >\"\$1\"" + ) >fake-editor-utf8-mime && + chmod +x fake-editor-utf8-mime && + echo y | \ + GIT_EDITOR="\"$(pwd)/fake-editor-utf8-mime\"" \ + GIT_SEND_EMAIL_NOTTY=1 \ + git send-email \ + --compose --subject foo \ + --from="Example " \ + --to=nobody@example.com \ + --smtp-server="$(pwd)/fake.sendmail" \ + $patches && + grep "^utf8 body" msgtxt1 && + grep "^Content-Type: text/plain; charset=iso-8859-1" msgtxt1 && + ! grep "^Content-Type: text/plain; charset=utf-8" msgtxt1 +' + +test_expect_success '--compose adds MIME for utf8 subject' ' + clean_fake_sendmail && + echo y | \ + GIT_EDITOR="\"$(pwd)/fake-editor\"" \ + GIT_SEND_EMAIL_NOTTY=1 \ + git send-email \ + --compose --subject utf8-sübjëct \ + --from="Example " \ + --to=nobody@example.com \ + --smtp-server="$(pwd)/fake.sendmail" \ + $patches && + grep "^fake edit" msgtxt1 && + grep "^Subject: =?utf-8?q?utf8-s=C3=BCbj=C3=ABct?=" msgtxt1 +' + test_done diff --git a/t/test-lib.sh b/t/test-lib.sh index f4703a3102..dee921aafe 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -368,7 +368,7 @@ test_done () { case "$test_failure" in 0) # We could: - # cd .. && rm -fr trash + # cd .. && rm -fr 'trash directory' # but that means we forbid any tests that use their own # subdirectory from calling test_done without coming back # to where they started from. @@ -419,7 +419,7 @@ rm -fr "$test" || { } test_create_repo "$test" -cd "$test" +cd "$test" || exit 1 # test for symbolic link capability ln -s x y 2> /dev/null && test -l y 2> /dev/null || no_symlinks=1 diff --git a/templates/hooks--prepare-commit-msg b/templates/hooks--prepare-commit-msg index ff0f42a1d9..d3c1da34d2 100644 --- a/templates/hooks--prepare-commit-msg +++ b/templates/hooks--prepare-commit-msg @@ -20,11 +20,11 @@ # The third example adds a Signed-off-by line to the message, that can # still be edited. This is rarely a good idea. -case "$2 $3" in - merge) - sed -i '/^Conflicts:/,/#/!b;s/^/# &/;s/^# #/#/' "$1" ;; +case "$2,$3" in + merge,) + perl -i -ne 's/^/# /, s/^# #/#/ if /^Conflicts/ .. /#/; print' "$1" ;; -# ""|template) +# ,|template,) # perl -i -pe ' # print "\n" . `git diff --cached --name-status -r` # if /^#/ && $first++ == 0' "$1" ;;