From 203439b2840c4c384060df2fa192994e4b6740ed Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:42 +0800 Subject: [PATCH 01/16] test: add test cases for relative_path Add subcommand "relative_path" in test-path-utils, and add test cases in t0060. Johannes tested an earlier version of this patch on Windows, and found that some relative_path tests should be skipped on Windows. This is because the bash on Windows rewrites arguments of regular Windows programs, such as git and the test helpers, if the arguments look like absolute POSIX paths. As a consequence, the actual tests performed are not what the tests scripts expect. The tests that need *not* be skipped are those where the two paths passed to 'test-path-utils relative_path' have the same prefix and the result is expected to be a relative path. This is because the rewriting changes "/a/b" to "D:/Src/MSysGit/a/b", and when both inputs are extended the same way, this just cancels out in the relative path computation. Signed-off-by: Jiang Xin Helped-by: Johannes Sixt Signed-off-by: Junio C Hamano --- t/t0060-path-utils.sh | 37 +++++++++++++++++++++++++++++++++++++ test-path-utils.c | 25 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 09a42a428e..72e89ce719 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -12,6 +12,11 @@ norm_path() { "test \"\$(test-path-utils normalize_path_copy '$1')\" = '$2'" } +relative_path() { + test_expect_success $4 "relative path: $1 $2 => $3" \ + "test \"\$(test-path-utils relative_path '$1' '$2')\" = '$3'" +} + # On Windows, we are using MSYS's bash, which mangles the paths. # Absolute paths are anchored at the MSYS installation directory, # which means that the path / accounts for this many characters: @@ -183,4 +188,36 @@ test_expect_success SYMLINKS 'real path works on symlinks' ' test "$sym" = "$(test-path-utils real_path "$dir2/syml")" ' +relative_path /a/b/c/ /a/b/ c/ +relative_path /a/b/c/ /a/b c/ +relative_path /a//b//c/ //a/b// c/ POSIX +relative_path /a/b /a/b . +relative_path /a/b/ /a/b . +relative_path /a /a/b /a POSIX +relative_path / /a/b/ / POSIX +relative_path /a/c /a/b/ /a/c POSIX +relative_path /a/c /a/b /a/c POSIX +relative_path /x/y /a/b/ /x/y POSIX +relative_path /a/b "" /a/b POSIX +relative_path /a/b "" /a/b POSIX +relative_path a/b/c/ a/b/ c/ +relative_path a/b/c/ a/b c/ +relative_path a/b//c a//b c +relative_path a/b/ a/b/ . +relative_path a/b/ a/b . +relative_path a a/b a # TODO: should be: .. +relative_path x/y a/b x/y # TODO: should be: ../../x/y +relative_path a/c a/b a/c # TODO: should be: ../c +relative_path a/b "" a/b +relative_path a/b "" a/b +relative_path "" /a/b "(empty)" +relative_path "" "" "(empty)" +relative_path "" "" "(empty)" +relative_path "" "" "(null)" +relative_path "" "" "(null)" + +test_expect_failure 'relative path: /a/b => segfault' ' + test-path-utils relative_path "" "/a/b" +' + test_done diff --git a/test-path-utils.c b/test-path-utils.c index 0092cbf354..8a6d22404e 100644 --- a/test-path-utils.c +++ b/test-path-utils.c @@ -28,6 +28,19 @@ static int normalize_ceiling_entry(struct string_list_item *item, void *unused) return 1; } +static void normalize_argv_string(const char **var, const char *input) +{ + if (!strcmp(input, "")) + *var = NULL; + else if (!strcmp(input, "")) + *var = ""; + else + *var = input; + + if (*var && (**var == '<' || **var == '(')) + die("Bad value: %s\n", input); +} + int main(int argc, char **argv) { if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) { @@ -103,6 +116,18 @@ int main(int argc, char **argv) return 0; } + if (argc == 4 && !strcmp(argv[1], "relative_path")) { + const char *in, *prefix, *rel; + normalize_argv_string(&in, argv[2]); + normalize_argv_string(&prefix, argv[3]); + rel = relative_path(in, prefix); + if (!rel) + puts("(null)"); + else + puts(strlen(rel) > 0 ? rel : "(empty)"); + return 0; + } + fprintf(stderr, "%s: unknown function name: %s\n", argv[0], argv[1] ? argv[1] : "(there was none)"); return 1; From e02ca72f70ed8f0268a81f72cb3230c72e538e77 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:43 +0800 Subject: [PATCH 02/16] path.c: refactor relative_path(), not only strip prefix Original design of relative_path() is simple, just strip the prefix (*base) from the absolute path (*abs). In most cases, we need a real relative path, such as: ../foo, ../../bar. That's why there is another reimplementation (path_relative()) in quote.c. Borrow some codes from path_relative() in quote.c to refactor relative_path() in path.c, so that it could return real relative path, and user can reuse this function without reimplementing his/her own. The function path_relative() in quote.c will be substituted, and I would use the new relative_path() function when implementing the interactive git-clean later. Different results for relative_path() before and after this refactor: abs path base path relative (original) relative (refactor) ======== ========= =================== =================== /a/b /a/b . ./ /a/b/ /a/b . ./ /a /a/b/ /a ../ / /a/b/ / ../../ /a/c /a/b/ /a/c ../c /x/y /a/b/ /x/y ../../x/y a/b/ a/b/ . ./ a/b/ a/b . ./ a a/b a ../ x/y a/b/ x/y ../../x/y a/c a/b a/c ../c (empty) (null) (empty) ./ (empty) (empty) (empty) ./ (empty) /a/b (empty) ./ (null) (null) (null) ./ (null) (empty) (null) ./ (null) /a/b (segfault) ./ You may notice that return value "." has been changed to "./". It is because: * Function quote_path_relative() in quote.c will show the relative path as "./" if abs(in) and base(prefix) are the same. * Function relative_path() is called only once (in setup.c), and it will be OK for the return value as "./" instead of ".". Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- cache.h | 2 +- path.c | 112 ++++++++++++++++++++++++++++++++---------- setup.c | 5 +- t/t0060-path-utils.sh | 39 +++++++-------- test-path-utils.c | 4 +- 5 files changed, 111 insertions(+), 51 deletions(-) diff --git a/cache.h b/cache.h index 94ca1acf70..8e42256942 100644 --- a/cache.h +++ b/cache.h @@ -737,7 +737,7 @@ int is_directory(const char *); const char *real_path(const char *path); const char *real_path_if_valid(const char *path); const char *absolute_path(const char *path); -const char *relative_path(const char *abs, const char *base); +const char *relative_path(const char *in, const char *prefix, struct strbuf *sb); int normalize_path_copy(char *dst, const char *src); int longest_ancestor_length(const char *path, struct string_list *prefixes); char *strip_path_suffix(const char *path, const char *suffix); diff --git a/path.c b/path.c index 04ff1487ed..7f3324aeea 100644 --- a/path.c +++ b/path.c @@ -441,42 +441,100 @@ int adjust_shared_perm(const char *path) return 0; } -const char *relative_path(const char *abs, const char *base) +/* + * Give path as relative to prefix. + * + * The strbuf may or may not be used, so do not assume it contains the + * returned path. + */ +const char *relative_path(const char *in, const char *prefix, + struct strbuf *sb) { - static char buf[PATH_MAX + 1]; + int in_len = in ? strlen(in) : 0; + int prefix_len = prefix ? strlen(prefix) : 0; + int in_off = 0; + int prefix_off = 0; int i = 0, j = 0; - if (!base || !base[0]) - return abs; - while (base[i]) { - if (is_dir_sep(base[i])) { - if (!is_dir_sep(abs[j])) - return abs; - while (is_dir_sep(base[i])) + if (!in_len) + return "./"; + else if (!prefix_len) + return in; + + while (i < prefix_len && j < in_len && prefix[i] == in[j]) { + if (is_dir_sep(prefix[i])) { + while (is_dir_sep(prefix[i])) i++; - while (is_dir_sep(abs[j])) + while (is_dir_sep(in[j])) j++; + prefix_off = i; + in_off = j; + } else { + i++; + j++; + } + } + + if ( + /* "prefix" seems like prefix of "in" */ + i >= prefix_len && + /* + * but "/foo" is not a prefix of "/foobar" + * (i.e. prefix not end with '/') + */ + prefix_off < prefix_len) { + if (j >= in_len) { + /* in="/a/b", prefix="/a/b" */ + in_off = in_len; + } else if (is_dir_sep(in[j])) { + /* in="/a/b/c", prefix="/a/b" */ + while (is_dir_sep(in[j])) + j++; + in_off = j; + } else { + /* in="/a/bbb/c", prefix="/a/b" */ + i = prefix_off; + } + } else if ( + /* "in" is short than "prefix" */ + j >= in_len && + /* "in" not end with '/' */ + in_off < in_len) { + if (is_dir_sep(prefix[i])) { + /* in="/a/b", prefix="/a/b/c/" */ + while (is_dir_sep(prefix[i])) + i++; + in_off = in_len; + } + } + in += in_off; + in_len -= in_off; + + if (i >= prefix_len) { + if (!in_len) + return "./"; + else + return in; + } + + strbuf_reset(sb); + strbuf_grow(sb, in_len); + + while (i < prefix_len) { + if (is_dir_sep(prefix[i])) { + strbuf_addstr(sb, "../"); + while (is_dir_sep(prefix[i])) + i++; continue; - } else if (abs[j] != base[i]) { - return abs; } i++; - j++; } - if ( - /* "/foo" is a prefix of "/foo" */ - abs[j] && - /* "/foo" is not a prefix of "/foobar" */ - !is_dir_sep(base[i-1]) && !is_dir_sep(abs[j]) - ) - return abs; - while (is_dir_sep(abs[j])) - j++; - if (!abs[j]) - strcpy(buf, "."); - else - strcpy(buf, abs + j); - return buf; + if (!is_dir_sep(prefix[prefix_len - 1])) + strbuf_addstr(sb, "../"); + + strbuf_addstr(sb, in); + + return sb->buf; } /* diff --git a/setup.c b/setup.c index 94c1e61bda..0d9ea6239f 100644 --- a/setup.c +++ b/setup.c @@ -360,6 +360,7 @@ int is_inside_work_tree(void) void setup_work_tree(void) { + struct strbuf sb = STRBUF_INIT; const char *work_tree, *git_dir; static int initialized = 0; @@ -379,8 +380,10 @@ void setup_work_tree(void) if (getenv(GIT_WORK_TREE_ENVIRONMENT)) setenv(GIT_WORK_TREE_ENVIRONMENT, ".", 1); - set_git_dir(relative_path(git_dir, work_tree)); + set_git_dir(relative_path(git_dir, work_tree, &sb)); initialized = 1; + + strbuf_release(&sb); } static int check_repository_format_gently(const char *gitdir, int *nongit_ok) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 72e89ce719..76c779252c 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -191,33 +191,30 @@ test_expect_success SYMLINKS 'real path works on symlinks' ' relative_path /a/b/c/ /a/b/ c/ relative_path /a/b/c/ /a/b c/ relative_path /a//b//c/ //a/b// c/ POSIX -relative_path /a/b /a/b . -relative_path /a/b/ /a/b . -relative_path /a /a/b /a POSIX -relative_path / /a/b/ / POSIX -relative_path /a/c /a/b/ /a/c POSIX -relative_path /a/c /a/b /a/c POSIX -relative_path /x/y /a/b/ /x/y POSIX +relative_path /a/b /a/b ./ +relative_path /a/b/ /a/b ./ +relative_path /a /a/b ../ +relative_path / /a/b/ ../../ +relative_path /a/c /a/b/ ../c +relative_path /a/c /a/b ../c +relative_path /x/y /a/b/ ../../x/y relative_path /a/b "" /a/b POSIX relative_path /a/b "" /a/b POSIX relative_path a/b/c/ a/b/ c/ relative_path a/b/c/ a/b c/ relative_path a/b//c a//b c -relative_path a/b/ a/b/ . -relative_path a/b/ a/b . -relative_path a a/b a # TODO: should be: .. -relative_path x/y a/b x/y # TODO: should be: ../../x/y -relative_path a/c a/b a/c # TODO: should be: ../c +relative_path a/b/ a/b/ ./ +relative_path a/b/ a/b ./ +relative_path a a/b ../ +relative_path x/y a/b ../../x/y +relative_path a/c a/b ../c relative_path a/b "" a/b relative_path a/b "" a/b -relative_path "" /a/b "(empty)" -relative_path "" "" "(empty)" -relative_path "" "" "(empty)" -relative_path "" "" "(null)" -relative_path "" "" "(null)" - -test_expect_failure 'relative path: /a/b => segfault' ' - test-path-utils relative_path "" "/a/b" -' +relative_path "" /a/b ./ +relative_path "" "" ./ +relative_path "" "" ./ +relative_path "" "" ./ +relative_path "" "" ./ +relative_path "" /a/b ./ test_done diff --git a/test-path-utils.c b/test-path-utils.c index 8a6d22404e..1bf4730619 100644 --- a/test-path-utils.c +++ b/test-path-utils.c @@ -117,14 +117,16 @@ int main(int argc, char **argv) } if (argc == 4 && !strcmp(argv[1], "relative_path")) { + struct strbuf sb = STRBUF_INIT; const char *in, *prefix, *rel; normalize_argv_string(&in, argv[2]); normalize_argv_string(&prefix, argv[3]); - rel = relative_path(in, prefix); + rel = relative_path(in, prefix, &sb); if (!rel) puts("(null)"); else puts(strlen(rel) > 0 ? rel : "(empty)"); + strbuf_release(&sb); return 0; } From ad66df2df14991e7436474d266cc6db823e6ae78 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:44 +0800 Subject: [PATCH 03/16] quote.c: substitute path_relative with relative_path Substitute the function path_relative in quote.c with the function relative_path. Function relative_path can be treated as an enhanced and more robust version of path_relative. Outputs of path_relative and it's replacement (relative_path) are the same for the following cases: path prefix output of path_relative output of relative_path ======== ========= ======================= ======================= /a/b/c/ /a/b/ c/ c/ /a/b/c /a/b/ c c /a/ /a/b/ ../ ../ / /a/b/ ../../ ../../ /a/c /a/b/ ../c ../c /x/y /a/b/ ../../x/y ../../x/y a/b/c/ a/b/ c/ c/ a/ a/b/ ../ ../ x/y a/b/ ../../x/y ../../x/y /a/b (empty) /a/b /a/b /a/b (null) /a/b /a/b a/b (empty) a/b a/b a/b (null) a/b a/b But if both of the path and the prefix are the same, or the returned relative path should be the current directory, the outputs of both functions are different. Function relative_path returns "./", while function path_relative returns empty string. path prefix output of path_relative output of relative_path ======== ========= ======================= ======================= /a/b/ /a/b/ (empty) ./ a/b/ a/b/ (empty) ./ (empty) (null) (empty) ./ (empty) (empty) (empty) ./ But the callers of path_relative can handle such cases, or never encounter this issue at all, because: * In function quote_path_relative, if the output of path_relative is empty, append "./" to it, like: if (!out->len) strbuf_addstr(out, "./"); * Another caller is write_name_quoted_relative, which is only used by builtin/ls-files.c. git-ls-files only show files, so path of files will never be identical with the prefix of a directory. The following differences show that path_relative does not handle extra slashes properly: path prefix output of path_relative output of relative_path ======== ========= ======================= ======================= /a//b//c/ //a/b// ../../../../a//b//c/ c/ a/b//c a//b ../b//c c And if prefix has no trailing slash, path_relative does not work properly either. But since prefix always has a trailing slash, it's not a problem. path prefix output of path_relative output of relative_path ======== ========= ======================= ======================= /a/b/c/ /a/b b/c/ c/ /a/b /a/b b ./ /a/b/ /a/b b/ ./ /a /a/b/ ../../a ../ a/b/c/ a/b b/c/ c/ a/b/ a/b b/ ./ a a/b ../a ../ x/y a/b/ ../x/y ../../x/y a/c a/b c ../c /a/ /a/b (empty) ../ (empty) /a/b ../../ ./ One tricky part in this conversion is write_name() function in ls-files.c. It takes a counted string, , that is to be made relative to and then quoted. Because write_name_quoted_relative() still takes these two parameters as counted string, but ignores the count and treat these two as NUL-terminated strings, this conversion needs to be audited for its callers: - For , all three callers of write_name() passes a NUL-terminated string and its true length, so this patch makes "len" unused. - For , prefix could be a string that is longer than empty while prefix_len could be 0 when "--full-name" option is used. This is fixed by checking prefix_len in write_name() and calling write_name_quoted_relative() with NULL when prefix_len is set to 0. Again, this makes "prefix_len" given to write_name_quoted_relative() unused, without introducing a bug. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/ls-files.c | 9 ++++++-- quote.c | 55 ++-------------------------------------------- 2 files changed, 9 insertions(+), 55 deletions(-) diff --git a/builtin/ls-files.c b/builtin/ls-files.c index 22020729cb..67e3713cd3 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -48,8 +48,13 @@ static const char *tag_resolve_undo = ""; static void write_name(const char* name, size_t len) { - write_name_quoted_relative(name, len, prefix, prefix_len, stdout, - line_terminator); + /* + * With "--full-name", prefix_len=0; write_name_quoted_relative() + * ignores prefix_len, so this caller needs to pass empty string + * in that case (a NULL is good for ""). + */ + write_name_quoted_relative(name, len, prefix_len ? prefix : NULL, + prefix_len, stdout, line_terminator); } static void show_dir_entry(const char *tag, struct dir_entry *ent) diff --git a/quote.c b/quote.c index 911229fdf3..64ff344158 100644 --- a/quote.c +++ b/quote.c @@ -312,75 +312,24 @@ void write_name_quotedpfx(const char *pfx, size_t pfxlen, fputc(terminator, fp); } -static const char *path_relative(const char *in, int len, - struct strbuf *sb, const char *prefix, - int prefix_len); - void write_name_quoted_relative(const char *name, size_t len, const char *prefix, size_t prefix_len, FILE *fp, int terminator) { struct strbuf sb = STRBUF_INIT; - name = path_relative(name, len, &sb, prefix, prefix_len); + name = relative_path(name, prefix, &sb); write_name_quoted(name, fp, terminator); strbuf_release(&sb); } -/* - * Give path as relative to prefix. - * - * The strbuf may or may not be used, so do not assume it contains the - * returned path. - */ -static const char *path_relative(const char *in, int len, - struct strbuf *sb, const char *prefix, - int prefix_len) -{ - int off, i; - - if (len < 0) - len = strlen(in); - if (prefix_len < 0) { - if (prefix) - prefix_len = strlen(prefix); - else - prefix_len = 0; - } - - off = 0; - i = 0; - while (i < prefix_len && i < len && prefix[i] == in[i]) { - if (prefix[i] == '/') - off = i + 1; - i++; - } - in += off; - len -= off; - - if (i >= prefix_len) - return in; - - strbuf_reset(sb); - strbuf_grow(sb, len); - - while (i < prefix_len) { - if (prefix[i] == '/') - strbuf_addstr(sb, "../"); - i++; - } - strbuf_add(sb, in, len); - - return sb->buf; -} - /* quote path as relative to the given prefix */ char *quote_path_relative(const char *in, int len, struct strbuf *out, const char *prefix) { struct strbuf sb = STRBUF_INIT; - const char *rel = path_relative(in, len, &sb, prefix, -1); + const char *rel = relative_path(in, prefix, &sb); strbuf_reset(out); quote_c_style_counted(rel, strlen(rel), out, NULL, 0); strbuf_release(&sb); From 39598f9983f759b5e38b9e762c695bad6c89a1b3 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:45 +0800 Subject: [PATCH 04/16] quote_path_relative(): remove redundant parameter quote_path_relative() used to take a counted string as its parameter (the string to be quoted). With an earlier change, it now uses relative_path() that does not take a counted string, and we have been passing only the pointer to the string since then. Remove the length parameter from quote_path_relative() to show that this parameter was redundant. All the changed lines show that the caller passed either -1 (to ask the function run strlen() on the string), or the length of the string, so the earlier conversion was safe. All the callers of quote_path_relative() that used to take counted string have been audited to make sure that they are passing length of the actual string (or -1 to ask the callee run strlen()) Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/clean.c | 18 +++++++++--------- builtin/grep.c | 5 ++--- builtin/ls-files.c | 2 +- quote.c | 7 ++----- quote.h | 4 ++-- wt-status.c | 17 ++++++++--------- 6 files changed, 24 insertions(+), 29 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index 04e396b17a..f77f95d7ad 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -56,7 +56,7 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) && !resolve_gitlink_ref(path->buf, "HEAD", submodule_head)) { if (!quiet) { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); printf(dry_run ? _(msg_would_skip_git_dir) : _(msg_skip_git_dir), quoted.buf); } @@ -70,7 +70,7 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, /* an empty dir could be removed even if it is unreadble */ res = dry_run ? 0 : rmdir(path->buf); if (res) { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); warning(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; } @@ -94,7 +94,7 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone)) ret = 1; if (gone) { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); string_list_append(&dels, quoted.buf); } else *dir_gone = 0; @@ -102,10 +102,10 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, } else { res = dry_run ? 0 : unlink(path->buf); if (!res) { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); string_list_append(&dels, quoted.buf); } else { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); warning(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; ret = 1; @@ -127,7 +127,7 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, if (!res) *dir_gone = 1; else { - quote_path_relative(path->buf, strlen(path->buf), "ed, prefix); + quote_path_relative(path->buf, prefix, "ed); warning(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; ret = 1; @@ -262,7 +262,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix) if (remove_dirs(&directory, prefix, rm_flags, dry_run, quiet, &gone)) errors++; if (gone && !quiet) { - qname = quote_path_relative(directory.buf, directory.len, &buf, prefix); + qname = quote_path_relative(directory.buf, prefix, &buf); printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); } } @@ -272,11 +272,11 @@ int cmd_clean(int argc, const char **argv, const char *prefix) continue; res = dry_run ? 0 : unlink(ent->name); if (res) { - qname = quote_path_relative(ent->name, -1, &buf, prefix); + qname = quote_path_relative(ent->name, prefix, &buf); warning(_(msg_warn_remove_failed), qname); errors++; } else if (!quiet) { - qname = quote_path_relative(ent->name, -1, &buf, prefix); + qname = quote_path_relative(ent->name, prefix, &buf); printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); } } diff --git a/builtin/grep.c b/builtin/grep.c index 159e65d47a..a419cda729 100644 --- a/builtin/grep.c +++ b/builtin/grep.c @@ -286,8 +286,7 @@ static int grep_sha1(struct grep_opt *opt, const unsigned char *sha1, struct strbuf pathbuf = STRBUF_INIT; if (opt->relative && opt->prefix_length) { - quote_path_relative(filename + tree_name_len, -1, &pathbuf, - opt->prefix); + quote_path_relative(filename + tree_name_len, opt->prefix, &pathbuf); strbuf_insert(&pathbuf, 0, filename, tree_name_len); } else { strbuf_addstr(&pathbuf, filename); @@ -318,7 +317,7 @@ static int grep_file(struct grep_opt *opt, const char *filename) struct strbuf buf = STRBUF_INIT; if (opt->relative && opt->prefix_length) - quote_path_relative(filename, -1, &buf, opt->prefix); + quote_path_relative(filename, opt->prefix, &buf); else strbuf_addstr(&buf, filename); diff --git a/builtin/ls-files.c b/builtin/ls-files.c index 67e3713cd3..48c82e8e9b 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -394,7 +394,7 @@ int report_path_error(const char *ps_matched, const char **pathspec, const char if (found_dup) continue; - name = quote_path_relative(pathspec[num], -1, &sb, prefix); + name = quote_path_relative(pathspec[num], prefix, &sb); error("pathspec '%s' did not match any file(s) known to git.", name); errors++; diff --git a/quote.c b/quote.c index 64ff344158..ebb835923f 100644 --- a/quote.c +++ b/quote.c @@ -325,8 +325,8 @@ void write_name_quoted_relative(const char *name, size_t len, } /* quote path as relative to the given prefix */ -char *quote_path_relative(const char *in, int len, - struct strbuf *out, const char *prefix) +char *quote_path_relative(const char *in, const char *prefix, + struct strbuf *out) { struct strbuf sb = STRBUF_INIT; const char *rel = relative_path(in, prefix, &sb); @@ -334,9 +334,6 @@ char *quote_path_relative(const char *in, int len, quote_c_style_counted(rel, strlen(rel), out, NULL, 0); strbuf_release(&sb); - if (!out->len) - strbuf_addstr(out, "./"); - return out->buf; } diff --git a/quote.h b/quote.h index 133155a48b..5610159c59 100644 --- a/quote.h +++ b/quote.h @@ -65,8 +65,8 @@ extern void write_name_quoted_relative(const char *name, size_t len, FILE *fp, int terminator); /* quote path as relative to the given prefix */ -extern char *quote_path_relative(const char *in, int len, - struct strbuf *out, const char *prefix); +extern char *quote_path_relative(const char *in, const char *prefix, + struct strbuf *out); /* quoting as a string literal for other languages */ extern void perl_quote_print(FILE *stream, const char *src); diff --git a/wt-status.c b/wt-status.c index bf84a86ee3..ef0fc4bb49 100644 --- a/wt-status.c +++ b/wt-status.c @@ -243,7 +243,7 @@ static void wt_status_print_unmerged_data(struct wt_status *s, struct strbuf onebuf = STRBUF_INIT; const char *one, *how = _("bug"); - one = quote_path(it->string, -1, &onebuf, s->prefix); + one = quote_path(it->string, s->prefix, &onebuf); status_printf(s, color(WT_STATUS_HEADER, s), "\t"); switch (d->stagemask) { case 1: how = _("both deleted:"); break; @@ -297,8 +297,8 @@ static void wt_status_print_change_data(struct wt_status *s, change_type); } - one = quote_path(one_name, -1, &onebuf, s->prefix); - two = quote_path(two_name, -1, &twobuf, s->prefix); + one = quote_path(one_name, s->prefix, &onebuf); + two = quote_path(two_name, s->prefix, &twobuf); status_printf(s, color(WT_STATUS_HEADER, s), "\t"); switch (status) { @@ -706,8 +706,7 @@ static void wt_status_print_other(struct wt_status *s, struct string_list_item *it; const char *path; it = &(l->items[i]); - path = quote_path(it->string, strlen(it->string), - &buf, s->prefix); + path = quote_path(it->string, s->prefix, &buf); if (column_active(s->colopts)) { string_list_append(&output, path); continue; @@ -1289,7 +1288,7 @@ static void wt_shortstatus_unmerged(struct string_list_item *it, } else { struct strbuf onebuf = STRBUF_INIT; const char *one; - one = quote_path(it->string, -1, &onebuf, s->prefix); + one = quote_path(it->string, s->prefix, &onebuf); printf(" %s\n", one); strbuf_release(&onebuf); } @@ -1317,7 +1316,7 @@ static void wt_shortstatus_status(struct string_list_item *it, struct strbuf onebuf = STRBUF_INIT; const char *one; if (d->head_path) { - one = quote_path(d->head_path, -1, &onebuf, s->prefix); + one = quote_path(d->head_path, s->prefix, &onebuf); if (*one != '"' && strchr(one, ' ') != NULL) { putchar('"'); strbuf_addch(&onebuf, '"'); @@ -1326,7 +1325,7 @@ static void wt_shortstatus_status(struct string_list_item *it, printf("%s -> ", one); strbuf_release(&onebuf); } - one = quote_path(it->string, -1, &onebuf, s->prefix); + one = quote_path(it->string, s->prefix, &onebuf); if (*one != '"' && strchr(one, ' ') != NULL) { putchar('"'); strbuf_addch(&onebuf, '"'); @@ -1345,7 +1344,7 @@ static void wt_shortstatus_other(struct string_list_item *it, } else { struct strbuf onebuf = STRBUF_INIT; const char *one; - one = quote_path(it->string, -1, &onebuf, s->prefix); + one = quote_path(it->string, s->prefix, &onebuf); color_fprintf(s->fp, color(WT_STATUS_UNTRACKED, s), "%s", sign); printf(" %s\n", one); strbuf_release(&onebuf); From e9a820cefde2170840fbcdf7c4b74369988869dc Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:46 +0800 Subject: [PATCH 05/16] write_name{_quoted_relative,}(): remove redundant parameters After substitute path_relative() in quote.c with relative_path() from path.c, parameters (such as len and prefix_len) are redundant in function write_name() and write_name_quoted_relative(). The callers have already been audited that the strings they pass are properly NUL terminated and the length they give are the length of the string (or -1 that asks the length to be counted by the callee). Remove these now-redundant parameters. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/ls-files.c | 17 ++++++++--------- quote.c | 3 +-- quote.h | 3 +-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/builtin/ls-files.c b/builtin/ls-files.c index 48c82e8e9b..29d45b4f5a 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -46,15 +46,14 @@ static const char *tag_modified = ""; static const char *tag_skip_worktree = ""; static const char *tag_resolve_undo = ""; -static void write_name(const char* name, size_t len) +static void write_name(const char *name) { /* - * With "--full-name", prefix_len=0; write_name_quoted_relative() - * ignores prefix_len, so this caller needs to pass empty string - * in that case (a NULL is good for ""). + * With "--full-name", prefix_len=0; this caller needs to pass + * an empty string in that case (a NULL is good for ""). */ - write_name_quoted_relative(name, len, prefix_len ? prefix : NULL, - prefix_len, stdout, line_terminator); + write_name_quoted_relative(name, prefix_len ? prefix : NULL, + stdout, line_terminator); } static void show_dir_entry(const char *tag, struct dir_entry *ent) @@ -68,7 +67,7 @@ static void show_dir_entry(const char *tag, struct dir_entry *ent) return; fputs(tag, stdout); - write_name(ent->name, ent->len); + write_name(ent->name); } static void show_other_files(struct dir_struct *dir) @@ -168,7 +167,7 @@ static void show_ce_entry(const char *tag, struct cache_entry *ce) find_unique_abbrev(ce->sha1,abbrev), ce_stage(ce)); } - write_name(ce->name, ce_namelen(ce)); + write_name(ce->name); if (debug_mode) { printf(" ctime: %d:%d\n", ce->ce_ctime.sec, ce->ce_ctime.nsec); printf(" mtime: %d:%d\n", ce->ce_mtime.sec, ce->ce_mtime.nsec); @@ -201,7 +200,7 @@ static void show_ru_info(void) printf("%s%06o %s %d\t", tag_resolve_undo, ui->mode[i], find_unique_abbrev(ui->sha1[i], abbrev), i + 1); - write_name(path, len); + write_name(path); } } } diff --git a/quote.c b/quote.c index ebb835923f..5c8808160e 100644 --- a/quote.c +++ b/quote.c @@ -312,8 +312,7 @@ void write_name_quotedpfx(const char *pfx, size_t pfxlen, fputc(terminator, fp); } -void write_name_quoted_relative(const char *name, size_t len, - const char *prefix, size_t prefix_len, +void write_name_quoted_relative(const char *name, const char *prefix, FILE *fp, int terminator) { struct strbuf sb = STRBUF_INIT; diff --git a/quote.h b/quote.h index 5610159c59..ed110a5d8d 100644 --- a/quote.h +++ b/quote.h @@ -60,8 +60,7 @@ extern void quote_two_c_style(struct strbuf *, const char *, const char *, int); extern void write_name_quoted(const char *name, FILE *, int terminator); extern void write_name_quotedpfx(const char *pfx, size_t pfxlen, const char *name, FILE *, int terminator); -extern void write_name_quoted_relative(const char *name, size_t len, - const char *prefix, size_t prefix_len, +extern void write_name_quoted_relative(const char *name, const char *prefix, FILE *fp, int terminator); /* quote path as relative to the given prefix */ From 396049e5fb62ea921379d02133e1ff00cc47bb3f Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:47 +0800 Subject: [PATCH 06/16] git-clean: refactor git-clean into two phases Before introducing interactive git-clean, refactor git-clean operations into two phases: * hold cleaning items in del_list, * and remove them in a separate loop at the end. We will introduce interactive git-clean between the two phases. The interactive git-clean will show what would be done and must confirm before do real cleaning. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/clean.c | 64 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index f77f95d7ad..77ec1f350f 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -15,6 +15,7 @@ #include "quote.h" static int force = -1; /* unset */ +static struct string_list del_list = STRING_LIST_INIT_DUP; static const char *const builtin_clean_usage[] = { N_("git clean [-d] [-f] [-n] [-q] [-e ] [-x | -X] [--] ..."), @@ -148,12 +149,13 @@ int cmd_clean(int argc, const char **argv, const char *prefix) int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0; int ignored_only = 0, config_set = 0, errors = 0, gone = 1; int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT; - struct strbuf directory = STRBUF_INIT; + struct strbuf abs_path = STRBUF_INIT; struct dir_struct dir; static const char **pathspec; struct strbuf buf = STRBUF_INIT; struct string_list exclude_list = STRING_LIST_INIT_NODUP; struct exclude_list *el; + struct string_list_item *item; const char *qname; char *seen = NULL; struct option options[] = { @@ -223,6 +225,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix) int matches = 0; struct cache_entry *ce; struct stat st; + const char *rel; /* * Remove the '/' at the end that directory @@ -242,13 +245,8 @@ int cmd_clean(int argc, const char **argv, const char *prefix) continue; /* Yup, this one exists unmerged */ } - /* - * we might have removed this as part of earlier - * recursive directory removal, so lstat() here could - * fail with ENOENT. - */ if (lstat(ent->name, &st)) - continue; + die_errno("Cannot lstat '%s'", ent->name); if (pathspec) { memset(seen, 0, argc > 0 ? argc : 1); @@ -257,33 +255,61 @@ int cmd_clean(int argc, const char **argv, const char *prefix) } if (S_ISDIR(st.st_mode)) { - strbuf_addstr(&directory, ent->name); if (remove_directories || (matches == MATCHED_EXACTLY)) { - if (remove_dirs(&directory, prefix, rm_flags, dry_run, quiet, &gone)) - errors++; - if (gone && !quiet) { - qname = quote_path_relative(directory.buf, prefix, &buf); - printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); - } + rel = relative_path(ent->name, prefix, &buf); + string_list_append(&del_list, rel); } - strbuf_reset(&directory); } else { if (pathspec && !matches) continue; - res = dry_run ? 0 : unlink(ent->name); + rel = relative_path(ent->name, prefix, &buf); + string_list_append(&del_list, rel); + } + } + + /* TODO: do interactive git-clean here, which will modify del_list */ + + for_each_string_list_item(item, &del_list) { + struct stat st; + + if (prefix) + strbuf_addstr(&abs_path, prefix); + + strbuf_addstr(&abs_path, item->string); + + /* + * we might have removed this as part of earlier + * recursive directory removal, so lstat() here could + * fail with ENOENT. + */ + if (lstat(abs_path.buf, &st)) + continue; + + if (S_ISDIR(st.st_mode)) { + if (remove_dirs(&abs_path, prefix, rm_flags, dry_run, quiet, &gone)) + errors++; + if (gone && !quiet) { + qname = quote_path_relative(item->string, NULL, &buf); + printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); + } + } else { + res = dry_run ? 0 : unlink(abs_path.buf); if (res) { - qname = quote_path_relative(ent->name, prefix, &buf); + qname = quote_path_relative(item->string, NULL, &buf); warning(_(msg_warn_remove_failed), qname); errors++; } else if (!quiet) { - qname = quote_path_relative(ent->name, prefix, &buf); + qname = quote_path_relative(item->string, NULL, &buf); printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); } } + strbuf_reset(&abs_path); } free(seen); - strbuf_release(&directory); + strbuf_release(&abs_path); + strbuf_release(&buf); + string_list_clear(&del_list, 0); string_list_clear(&exclude_list, 0); return (errors != 0); } From 17696002086e8c6b9e998543d212e707c7d511ab Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:48 +0800 Subject: [PATCH 07/16] git-clean: add support for -i/--interactive Show what would be done and the user must confirm before actually cleaning. Would remove ... Would remove ... Would remove ... Remove [y/n]? Press "y" to start cleaning, and press "n" if you want to abort. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- Documentation/git-clean.txt | 10 +++++-- builtin/clean.c | 57 +++++++++++++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/Documentation/git-clean.txt b/Documentation/git-clean.txt index bdc3ab80c7..186e3455e4 100644 --- a/Documentation/git-clean.txt +++ b/Documentation/git-clean.txt @@ -8,7 +8,7 @@ git-clean - Remove untracked files from the working tree SYNOPSIS -------- [verse] -'git clean' [-d] [-f] [-n] [-q] [-e ] [-x | -X] [--] ... +'git clean' [-d] [-f] [-i] [-n] [-q] [-e ] [-x | -X] [--] ... DESCRIPTION ----------- @@ -34,7 +34,13 @@ OPTIONS -f:: --force:: If the Git configuration variable clean.requireForce is not set - to false, 'git clean' will refuse to run unless given -f or -n. + to false, 'git clean' will refuse to run unless given -f, -n or + -i. + +-i:: +--interactive:: + Show what would be done and the user must confirm before actually + cleaning. -n:: --dry-run:: diff --git a/builtin/clean.c b/builtin/clean.c index 77ec1f350f..698fb1ba14 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -15,10 +15,11 @@ #include "quote.h" static int force = -1; /* unset */ +static int interactive; static struct string_list del_list = STRING_LIST_INIT_DUP; static const char *const builtin_clean_usage[] = { - N_("git clean [-d] [-f] [-n] [-q] [-e ] [-x | -X] [--] ..."), + N_("git clean [-d] [-f] [-i] [-n] [-q] [-e ] [-x | -X] [--] ..."), NULL }; @@ -143,6 +144,50 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, return ret; } +static void interactive_main_loop(void) +{ + struct strbuf confirm = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + const char *qname; + + while (del_list.nr) { + putchar('\n'); + for_each_string_list_item(item, &del_list) { + qname = quote_path_relative(item->string, NULL, &buf); + printf(_(msg_would_remove), qname); + } + putchar('\n'); + + printf(_("Remove [y/n]? ")); + if (strbuf_getline(&confirm, stdin, '\n') != EOF) { + strbuf_trim(&confirm); + } else { + /* Ctrl-D is the same as "quit" */ + string_list_clear(&del_list, 0); + putchar('\n'); + printf_ln("Bye."); + break; + } + + if (confirm.len) { + if (!strncasecmp(confirm.buf, "yes", confirm.len)) { + break; + } else if (!strncasecmp(confirm.buf, "no", confirm.len) || + !strncasecmp(confirm.buf, "quit", confirm.len)) { + string_list_clear(&del_list, 0); + printf_ln("Bye."); + break; + } else { + continue; + } + } + } + + strbuf_release(&buf); + strbuf_release(&confirm); +} + int cmd_clean(int argc, const char **argv, const char *prefix) { int i, res; @@ -162,6 +207,7 @@ int cmd_clean(int argc, const char **argv, const char *prefix) OPT__QUIET(&quiet, N_("do not print names of files removed")), OPT__DRY_RUN(&dry_run, N_("dry run")), OPT__FORCE(&force, N_("force")), + OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")), OPT_BOOLEAN('d', NULL, &remove_directories, N_("remove whole directories")), { OPTION_CALLBACK, 'e', "exclude", &exclude_list, N_("pattern"), @@ -188,12 +234,12 @@ int cmd_clean(int argc, const char **argv, const char *prefix) if (ignored && ignored_only) die(_("-x and -X cannot be used together")); - if (!dry_run && !force) { + if (!interactive && !dry_run && !force) { if (config_set) - die(_("clean.requireForce set to true and neither -n nor -f given; " + die(_("clean.requireForce set to true and neither -i, -n nor -f given; " "refusing to clean")); else - die(_("clean.requireForce defaults to true and neither -n nor -f given; " + die(_("clean.requireForce defaults to true and neither -i, -n nor -f given; " "refusing to clean")); } @@ -267,7 +313,8 @@ int cmd_clean(int argc, const char **argv, const char *prefix) } } - /* TODO: do interactive git-clean here, which will modify del_list */ + if (interactive && del_list.nr > 0) + interactive_main_loop(); for_each_string_list_item(item, &del_list) { struct stat st; From 1b8fd46732fc2e4e8300c11057a7fa9a8c2bc1b4 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:49 +0800 Subject: [PATCH 08/16] git-clean: show items of del_list in columns When there are lots of items to be cleaned, it is hard to see them all in one screen. Show them in columns will solve this problem. Signed-off-by: Jiang Xin Comments-by: Matthieu Moy Signed-off-by: Junio C Hamano --- Documentation/config.txt | 4 ++++ builtin/clean.c | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Documentation/config.txt b/Documentation/config.txt index 6e53fc5074..e031b3b28a 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -955,6 +955,10 @@ column.branch:: Specify whether to output branch listing in `git branch` in columns. See `column.ui` for details. +column.clean:: + Specify the layout when list items in `git clean -i`, which always + shows files and directories in columns. See `column.ui` for details. + column.status:: Specify whether to output untracked files in `git status` in columns. See `column.ui` for details. diff --git a/builtin/clean.c b/builtin/clean.c index 698fb1ba14..75cc6a878c 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -13,10 +13,12 @@ #include "refs.h" #include "string-list.h" #include "quote.h" +#include "column.h" static int force = -1; /* unset */ static int interactive; static struct string_list del_list = STRING_LIST_INIT_DUP; +static unsigned int colopts; static const char *const builtin_clean_usage[] = { N_("git clean [-d] [-f] [-i] [-n] [-q] [-e ] [-x | -X] [--] ..."), @@ -31,8 +33,13 @@ static const char *msg_warn_remove_failed = N_("failed to remove %s"); static int git_clean_config(const char *var, const char *value, void *cb) { - if (!strcmp(var, "clean.requireforce")) + if (!prefixcmp(var, "column.")) + return git_column_config(var, value, "clean", &colopts); + + if (!strcmp(var, "clean.requireforce")) { force = !git_config_bool(var, value); + return 0; + } return git_default_config(var, value, cb); } @@ -144,21 +151,46 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, return ret; } +static void pretty_print_dels(void) +{ + struct string_list list = STRING_LIST_INIT_DUP; + struct string_list_item *item; + struct strbuf buf = STRBUF_INIT; + const char *qname; + struct column_options copts; + + for_each_string_list_item(item, &del_list) { + qname = quote_path_relative(item->string, NULL, &buf); + string_list_append(&list, qname); + } + + /* + * always enable column display, we only consult column.* + * about layout strategy and stuff + */ + colopts = (colopts & ~COL_ENABLE_MASK) | COL_ENABLED; + memset(&copts, 0, sizeof(copts)); + copts.indent = " "; + copts.padding = 2; + print_columns(&list, colopts, &copts); + putchar('\n'); + strbuf_release(&buf); + string_list_clear(&list, 0); +} + static void interactive_main_loop(void) { struct strbuf confirm = STRBUF_INIT; - struct strbuf buf = STRBUF_INIT; - struct string_list_item *item; - const char *qname; while (del_list.nr) { putchar('\n'); - for_each_string_list_item(item, &del_list) { - qname = quote_path_relative(item->string, NULL, &buf); - printf(_(msg_would_remove), qname); - } + printf_ln(Q_("Would remove the following item:", + "Would remove the following items:", + del_list.nr)); putchar('\n'); + pretty_print_dels(); + printf(_("Remove [y/n]? ")); if (strbuf_getline(&confirm, stdin, '\n') != EOF) { strbuf_trim(&confirm); @@ -184,7 +216,6 @@ static void interactive_main_loop(void) } } - strbuf_release(&buf); strbuf_release(&confirm); } From 7a9b0b802ecf5a09502b46de27c07ff05f55b247 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:50 +0800 Subject: [PATCH 09/16] git-clean: add colors to interactive git-clean Show header, help, error messages, and prompt in colors for interactive git-clean. Re-use config variables, such as "color.interactive" and "color.interactive." for command `git-add--interactive`. Signed-off-by: Jiang Xin Comments-by: Matthieu Moy Signed-off-by: Junio C Hamano --- Documentation/config.txt | 17 +++++----- builtin/clean.c | 73 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/Documentation/config.txt b/Documentation/config.txt index e031b3b28a..836138071a 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -876,16 +876,17 @@ The values of these variables may be specified as in color.branch.. color.interactive:: When set to `always`, always use colors for interactive prompts - and displays (such as those used by "git-add --interactive"). - When false (or `never`), never. When set to `true` or `auto`, use - colors only when the output is to the terminal. Defaults to false. + and displays (such as those used by "git-add --interactive" and + "git-clean --interactive"). When false (or `never`), never. + When set to `true` or `auto`, use colors only when the output is + to the terminal. Defaults to false. color.interactive.:: - Use customized color for 'git add --interactive' - output. `` may be `prompt`, `header`, `help` or `error`, for - four distinct types of normal output from interactive - commands. The values of these variables may be specified as - in color.branch.. + Use customized color for 'git add --interactive' and 'git clean + --interactive' output. `` may be `prompt`, `header`, `help` + or `error`, for four distinct types of normal output from + interactive commands. The values of these variables may be + specified as in color.branch.. color.pager:: A boolean to enable/disable colored output when the pager is in diff --git a/builtin/clean.c b/builtin/clean.c index 75cc6a878c..dfa99bfd5d 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -14,6 +14,7 @@ #include "string-list.h" #include "quote.h" #include "column.h" +#include "color.h" static int force = -1; /* unset */ static int interactive; @@ -31,16 +32,82 @@ static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); static const char *msg_warn_remove_failed = N_("failed to remove %s"); +static int clean_use_color = -1; +static char clean_colors[][COLOR_MAXLEN] = { + GIT_COLOR_RESET, + GIT_COLOR_NORMAL, /* PLAIN */ + GIT_COLOR_BOLD_BLUE, /* PROMPT */ + GIT_COLOR_BOLD, /* HEADER */ + GIT_COLOR_BOLD_RED, /* HELP */ + GIT_COLOR_BOLD_RED, /* ERROR */ +}; +enum color_clean { + CLEAN_COLOR_RESET = 0, + CLEAN_COLOR_PLAIN = 1, + CLEAN_COLOR_PROMPT = 2, + CLEAN_COLOR_HEADER = 3, + CLEAN_COLOR_HELP = 4, + CLEAN_COLOR_ERROR = 5, +}; + +static int parse_clean_color_slot(const char *var) +{ + if (!strcasecmp(var, "reset")) + return CLEAN_COLOR_RESET; + if (!strcasecmp(var, "plain")) + return CLEAN_COLOR_PLAIN; + if (!strcasecmp(var, "prompt")) + return CLEAN_COLOR_PROMPT; + if (!strcasecmp(var, "header")) + return CLEAN_COLOR_HEADER; + if (!strcasecmp(var, "help")) + return CLEAN_COLOR_HELP; + if (!strcasecmp(var, "error")) + return CLEAN_COLOR_ERROR; + return -1; +} + static int git_clean_config(const char *var, const char *value, void *cb) { if (!prefixcmp(var, "column.")) return git_column_config(var, value, "clean", &colopts); + /* honors the color.interactive* config variables which also + applied in git-add--interactive and git-stash */ + if (!strcmp(var, "color.interactive")) { + clean_use_color = git_config_colorbool(var, value); + return 0; + } + if (!prefixcmp(var, "color.interactive.")) { + int slot = parse_clean_color_slot(var + + strlen("color.interactive.")); + if (slot < 0) + return 0; + if (!value) + return config_error_nonbool(var); + color_parse(value, var, clean_colors[slot]); + return 0; + } + if (!strcmp(var, "clean.requireforce")) { force = !git_config_bool(var, value); return 0; } - return git_default_config(var, value, cb); + + /* inspect the color.ui config variable and others */ + return git_color_default_config(var, value, cb); +} + +static const char *clean_get_color(enum color_clean ix) +{ + if (want_color(clean_use_color)) + return clean_colors[ix]; + return ""; +} + +static void clean_print_color(enum color_clean ix) +{ + printf("%s", clean_get_color(ix)); } static int exclude_cb(const struct option *opt, const char *arg, int unset) @@ -184,14 +251,18 @@ static void interactive_main_loop(void) while (del_list.nr) { putchar('\n'); + clean_print_color(CLEAN_COLOR_HEADER); printf_ln(Q_("Would remove the following item:", "Would remove the following items:", del_list.nr)); + clean_print_color(CLEAN_COLOR_RESET); putchar('\n'); pretty_print_dels(); + clean_print_color(CLEAN_COLOR_PROMPT); printf(_("Remove [y/n]? ")); + clean_print_color(CLEAN_COLOR_RESET); if (strbuf_getline(&confirm, stdin, '\n') != EOF) { strbuf_trim(&confirm); } else { From 9f93e4611f72577306e369a64d0a4da847be9751 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:51 +0800 Subject: [PATCH 10/16] git-clean: use a git-add-interactive compatible UI Rewrite menu using a new method `list_and_choose`, which is borrowed from `git-add--interactive.perl`. We will use this framework to add new actions for interactive git-clean later. Please NOTE: * Method `list_and_choose` return an array of integers, and * it is up to you to free the allocated memory of the array. * The array ends with EOF. * If user pressed CTRL-D (i.e. EOF), no selection returned. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/clean.c | 454 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 426 insertions(+), 28 deletions(-) diff --git a/builtin/clean.c b/builtin/clean.c index dfa99bfd5d..df887a8a96 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -50,6 +50,36 @@ enum color_clean { CLEAN_COLOR_ERROR = 5, }; +#define MENU_OPTS_SINGLETON 01 +#define MENU_OPTS_IMMEDIATE 02 +#define MENU_OPTS_LIST_ONLY 04 + +struct menu_opts { + const char *header; + const char *prompt; + int flags; +}; + +#define MENU_RETURN_NO_LOOP 10 + +struct menu_item { + char hotkey; + const char *title; + int selected; + int (*fn)(); +}; + +enum menu_stuff_type { + MENU_STUFF_TYPE_STRING_LIST = 1, + MENU_STUFF_TYPE_MENU_ITEM +}; + +struct menu_stuff { + enum menu_stuff_type type; + int nr; + void *stuff; +}; + static int parse_clean_color_slot(const char *var) { if (!strcasecmp(var, "reset")) @@ -240,54 +270,422 @@ static void pretty_print_dels(void) copts.indent = " "; copts.padding = 2; print_columns(&list, colopts, &copts); - putchar('\n'); strbuf_release(&buf); string_list_clear(&list, 0); } +static void pretty_print_menus(struct string_list *menu_list) +{ + unsigned int local_colopts = 0; + struct column_options copts; + + local_colopts = COL_ENABLED | COL_ROW; + memset(&copts, 0, sizeof(copts)); + copts.indent = " "; + copts.padding = 2; + print_columns(menu_list, local_colopts, &copts); +} + +static void prompt_help_cmd(int singleton) +{ + clean_print_color(CLEAN_COLOR_HELP); + printf_ln(singleton ? + _("Prompt help:\n" + "1 - select a numbered item\n" + "foo - select item based on unique prefix\n" + " - (empty) select nothing") : + _("Prompt help:\n" + "1 - select a single item\n" + "3-5 - select a range of items\n" + "2-3,6-9 - select multiple ranges\n" + "foo - select item based on unique prefix\n" + "-... - unselect specified items\n" + "* - choose all items\n" + " - (empty) finish selecting")); + clean_print_color(CLEAN_COLOR_RESET); +} + +/* + * display menu stuff with number prefix and hotkey highlight + */ +static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen) +{ + struct string_list menu_list = STRING_LIST_INIT_DUP; + struct strbuf menu = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct menu_item *menu_item; + struct string_list_item *string_list_item; + int i; + + switch (stuff->type) { + default: + die("Bad type of menu_staff when print menu"); + case MENU_STUFF_TYPE_MENU_ITEM: + menu_item = (struct menu_item *)stuff->stuff; + for (i = 0; i < stuff->nr; i++, menu_item++) { + const char *p; + int highlighted = 0; + + p = menu_item->title; + if ((*chosen)[i] < 0) + (*chosen)[i] = menu_item->selected ? 1 : 0; + strbuf_addf(&menu, "%s%2d: ", (*chosen)[i] ? "*" : " ", i+1); + for (; *p; p++) { + if (!highlighted && *p == menu_item->hotkey) { + strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_PROMPT)); + strbuf_addch(&menu, *p); + strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_RESET)); + highlighted = 1; + } else { + strbuf_addch(&menu, *p); + } + } + string_list_append(&menu_list, menu.buf); + strbuf_reset(&menu); + } + break; + case MENU_STUFF_TYPE_STRING_LIST: + i = 0; + for_each_string_list_item(string_list_item, (struct string_list *)stuff->stuff) { + if ((*chosen)[i] < 0) + (*chosen)[i] = 0; + strbuf_addf(&menu, "%s%2d: %s", + (*chosen)[i] ? "*" : " ", i+1, string_list_item->string); + string_list_append(&menu_list, menu.buf); + strbuf_reset(&menu); + i++; + } + break; + } + + pretty_print_menus(&menu_list); + + strbuf_release(&menu); + strbuf_release(&buf); + string_list_clear(&menu_list, 0); +} + +/* + * Parse user input, and return choice(s) for menu (menu_stuff). + * + * Input + * (for single choice) + * 1 - select a numbered item + * foo - select item based on menu title + * - (empty) select nothing + * + * (for multiple choice) + * 1 - select a single item + * 3-5 - select a range of items + * 2-3,6-9 - select multiple ranges + * foo - select item based on menu title + * -... - unselect specified items + * * - choose all items + * - (empty) finish selecting + * + * The parse result will be saved in array **chosen, and + * return number of total selections. + */ +static int parse_choice(struct menu_stuff *menu_stuff, + int is_single, + struct strbuf input, + int **chosen) +{ + struct strbuf **choice_list, **ptr; + struct menu_item *menu_item; + struct string_list_item *string_list_item; + int nr = 0; + int i; + + if (is_single) { + choice_list = strbuf_split_max(&input, '\n', 0); + } else { + char *p = input.buf; + do { + if (*p == ',') + *p = ' '; + } while (*p++); + choice_list = strbuf_split_max(&input, ' ', 0); + } + + for (ptr = choice_list; *ptr; ptr++) { + char *p; + int choose = 1; + int bottom = 0, top = 0; + int is_range, is_number; + + strbuf_trim(*ptr); + if (!(*ptr)->len) + continue; + + /* Input that begins with '-'; unchoose */ + if (*(*ptr)->buf == '-') { + choose = 0; + strbuf_remove((*ptr), 0, 1); + } + + is_range = 0; + is_number = 1; + for (p = (*ptr)->buf; *p; p++) { + if ('-' == *p) { + if (!is_range) { + is_range = 1; + is_number = 0; + } else { + is_number = 0; + is_range = 0; + break; + } + } else if (!isdigit(*p)) { + is_number = 0; + is_range = 0; + break; + } + } + + if (is_number) { + bottom = atoi((*ptr)->buf); + top = bottom; + } else if (is_range) { + bottom = atoi((*ptr)->buf); + /* a range can be specified like 5-7 or 5- */ + if (!*(strchr((*ptr)->buf, '-') + 1)) + top = menu_stuff->nr; + else + top = atoi(strchr((*ptr)->buf, '-') + 1); + } else if (!strcmp((*ptr)->buf, "*")) { + bottom = 1; + top = menu_stuff->nr; + } else { + switch (menu_stuff->type) { + default: + die("Bad type of menu_stuff when parse choice"); + case MENU_STUFF_TYPE_MENU_ITEM: + menu_item = (struct menu_item *)menu_stuff->stuff; + for (i = 0; i < menu_stuff->nr; i++, menu_item++) { + if (((*ptr)->len == 1 && + *(*ptr)->buf == menu_item->hotkey) || + !strcasecmp((*ptr)->buf, menu_item->title)) { + bottom = i + 1; + top = bottom; + break; + } + } + break; + case MENU_STUFF_TYPE_STRING_LIST: + string_list_item = ((struct string_list *)menu_stuff->stuff)->items; + for (i = 0; i < menu_stuff->nr; i++, string_list_item++) { + if (!strcasecmp((*ptr)->buf, string_list_item->string)) { + bottom = i + 1; + top = bottom; + break; + } + } + break; + } + } + + if (top <= 0 || bottom <= 0 || top > menu_stuff->nr || bottom > top || + (is_single && bottom != top)) { + clean_print_color(CLEAN_COLOR_ERROR); + printf_ln(_("Huh (%s)?"), (*ptr)->buf); + clean_print_color(CLEAN_COLOR_RESET); + continue; + } + + for (i = bottom; i <= top; i++) + (*chosen)[i-1] = choose; + } + + strbuf_list_free(choice_list); + + for (i = 0; i < menu_stuff->nr; i++) + nr += (*chosen)[i]; + return nr; +} + +/* + * Implement a git-add-interactive compatible UI, which is borrowed + * from git-add--interactive.perl. + * + * Return value: + * + * - Return an array of integers + * - , and it is up to you to free the allocated memory. + * - The array ends with EOF. + * - If user pressed CTRL-D (i.e. EOF), no selection returned. + */ +static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) +{ + struct strbuf choice = STRBUF_INIT; + int *chosen, *result; + int nr = 0; + int eof = 0; + int i; + + chosen = xmalloc(sizeof(int) * stuff->nr); + /* set chosen as uninitialized */ + for (i = 0; i < stuff->nr; i++) + chosen[i] = -1; + + for (;;) { + if (opts->header) { + printf_ln("%s%s%s", + clean_get_color(CLEAN_COLOR_HEADER), + _(opts->header), + clean_get_color(CLEAN_COLOR_RESET)); + } + + /* chosen will be initialized by print_highlight_menu_stuff */ + print_highlight_menu_stuff(stuff, &chosen); + + if (opts->flags & MENU_OPTS_LIST_ONLY) + break; + + if (opts->prompt) { + printf("%s%s%s%s", + clean_get_color(CLEAN_COLOR_PROMPT), + _(opts->prompt), + opts->flags & MENU_OPTS_SINGLETON ? "> " : ">> ", + clean_get_color(CLEAN_COLOR_RESET)); + } + + if (strbuf_getline(&choice, stdin, '\n') != EOF) { + strbuf_trim(&choice); + } else { + eof = 1; + break; + } + + /* help for prompt */ + if (!strcmp(choice.buf, "?")) { + prompt_help_cmd(opts->flags & MENU_OPTS_SINGLETON); + continue; + } + + /* for a multiple-choice menu, press ENTER (empty) will return back */ + if (!(opts->flags & MENU_OPTS_SINGLETON) && !choice.len) + break; + + nr = parse_choice(stuff, + opts->flags & MENU_OPTS_SINGLETON, + choice, + &chosen); + + if (opts->flags & MENU_OPTS_SINGLETON) { + if (nr) + break; + } else if (opts->flags & MENU_OPTS_IMMEDIATE) { + break; + } + } + + if (eof) { + result = xmalloc(sizeof(int)); + *result = EOF; + } else { + int j = 0; + + /* + * recalculate nr, if return back from menu directly with + * default selections. + */ + if (!nr) { + for (i = 0; i < stuff->nr; i++) + nr += chosen[i]; + } + + result = xmalloc(sizeof(int) * (nr + 1)); + memset(result, 0, sizeof(int) * (nr + 1)); + for (i = 0; i < stuff->nr && j < nr; i++) { + if (chosen[i]) + result[j++] = i; + } + result[j] = EOF; + } + + free(chosen); + strbuf_release(&choice); + return result; +} + +static int clean_cmd(void) +{ + return MENU_RETURN_NO_LOOP; +} + +static int quit_cmd(void) +{ + string_list_clear(&del_list, 0); + printf_ln(_("Bye.")); + return MENU_RETURN_NO_LOOP; +} + +static int help_cmd(void) +{ + clean_print_color(CLEAN_COLOR_HELP); + printf_ln(_( + "clean - start cleaning\n" + "quit - stop cleaning\n" + "help - this screen\n" + "? - help for prompt selection" + )); + clean_print_color(CLEAN_COLOR_RESET); + return 0; +} + static void interactive_main_loop(void) { - struct strbuf confirm = STRBUF_INIT; - while (del_list.nr) { - putchar('\n'); + struct menu_opts menu_opts; + struct menu_stuff menu_stuff; + struct menu_item menus[] = { + {'c', "clean", 0, clean_cmd}, + {'q', "quit", 0, quit_cmd}, + {'h', "help", 0, help_cmd}, + }; + int *chosen; + + menu_opts.header = N_("*** Commands ***"); + menu_opts.prompt = N_("What now"); + menu_opts.flags = MENU_OPTS_SINGLETON; + + menu_stuff.type = MENU_STUFF_TYPE_MENU_ITEM; + menu_stuff.stuff = menus; + menu_stuff.nr = sizeof(menus) / sizeof(struct menu_item); + clean_print_color(CLEAN_COLOR_HEADER); printf_ln(Q_("Would remove the following item:", "Would remove the following items:", del_list.nr)); clean_print_color(CLEAN_COLOR_RESET); - putchar('\n'); pretty_print_dels(); - clean_print_color(CLEAN_COLOR_PROMPT); - printf(_("Remove [y/n]? ")); - clean_print_color(CLEAN_COLOR_RESET); - if (strbuf_getline(&confirm, stdin, '\n') != EOF) { - strbuf_trim(&confirm); - } else { - /* Ctrl-D is the same as "quit" */ - string_list_clear(&del_list, 0); - putchar('\n'); - printf_ln("Bye."); - break; - } + chosen = list_and_choose(&menu_opts, &menu_stuff); - if (confirm.len) { - if (!strncasecmp(confirm.buf, "yes", confirm.len)) { - break; - } else if (!strncasecmp(confirm.buf, "no", confirm.len) || - !strncasecmp(confirm.buf, "quit", confirm.len)) { - string_list_clear(&del_list, 0); - printf_ln("Bye."); - break; - } else { + if (*chosen != EOF) { + int ret; + ret = menus[*chosen].fn(); + if (ret != MENU_RETURN_NO_LOOP) { + free(chosen); + chosen = NULL; + if (!del_list.nr) { + clean_print_color(CLEAN_COLOR_ERROR); + printf_ln(_("No more files to clean, exiting.")); + clean_print_color(CLEAN_COLOR_RESET); + break; + } continue; } + } else { + quit_cmd(); } - } - strbuf_release(&confirm); + free(chosen); + chosen = NULL; + break; + } } int cmd_clean(int argc, const char **argv, const char *prefix) From d1239264f2786d7ea15543b980c6bf6afbc4701a Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:52 +0800 Subject: [PATCH 11/16] git-clean: add filter by pattern interactive action Add a new action for interactive git-clean: filter by pattern. When the user chooses this action, user can input space-separated patterns (the same syntax as gitignore), and each clean candidate that matches with one of the patterns will be excluded from cleaning. When the user feels it's OK, presses ENTER and backs to the confirmation dialog. Signed-off-by: Jiang Xin Suggested-by: Junio C Hamano Signed-off-by: Junio C Hamano --- builtin/clean.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index df887a8a96..363690299c 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -614,6 +614,72 @@ static int clean_cmd(void) return MENU_RETURN_NO_LOOP; } +static int filter_by_patterns_cmd(void) +{ + struct dir_struct dir; + struct strbuf confirm = STRBUF_INIT; + struct strbuf **ignore_list; + struct string_list_item *item; + struct exclude_list *el; + int changed = -1, i; + + for (;;) { + if (!del_list.nr) + break; + + if (changed) + pretty_print_dels(); + + clean_print_color(CLEAN_COLOR_PROMPT); + printf(_("Input ignore patterns>> ")); + clean_print_color(CLEAN_COLOR_RESET); + if (strbuf_getline(&confirm, stdin, '\n') != EOF) + strbuf_trim(&confirm); + else + putchar('\n'); + + /* quit filter_by_pattern mode if press ENTER or Ctrl-D */ + if (!confirm.len) + break; + + memset(&dir, 0, sizeof(dir)); + el = add_exclude_list(&dir, EXC_CMDL, "manual exclude"); + ignore_list = strbuf_split_max(&confirm, ' ', 0); + + for (i = 0; ignore_list[i]; i++) { + strbuf_trim(ignore_list[i]); + if (!ignore_list[i]->len) + continue; + + add_exclude(ignore_list[i]->buf, "", 0, el, -(i+1)); + } + + changed = 0; + for_each_string_list_item(item, &del_list) { + int dtype = DT_UNKNOWN; + + if (is_excluded(&dir, item->string, &dtype)) { + *item->string = '\0'; + changed++; + } + } + + if (changed) { + string_list_remove_empty_items(&del_list, 0); + } else { + clean_print_color(CLEAN_COLOR_ERROR); + printf_ln(_("WARNING: Cannot find items matched by: %s"), confirm.buf); + clean_print_color(CLEAN_COLOR_RESET); + } + + strbuf_list_free(ignore_list); + clear_directory(&dir); + } + + strbuf_release(&confirm); + return 0; +} + static int quit_cmd(void) { string_list_clear(&del_list, 0); @@ -626,6 +692,7 @@ static int help_cmd(void) clean_print_color(CLEAN_COLOR_HELP); printf_ln(_( "clean - start cleaning\n" + "filter by pattern - exclude items from deletion\n" "quit - stop cleaning\n" "help - this screen\n" "? - help for prompt selection" @@ -641,6 +708,7 @@ static void interactive_main_loop(void) struct menu_stuff menu_stuff; struct menu_item menus[] = { {'c', "clean", 0, clean_cmd}, + {'f', "filter by pattern", 0, filter_by_patterns_cmd}, {'q', "quit", 0, quit_cmd}, {'h', "help", 0, help_cmd}, }; From c1f1d24aa5c72821096e2a31aaf4175329ca0260 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:53 +0800 Subject: [PATCH 12/16] git-clean: add select by numbers interactive action Draw a multiple choice menu using `list_and_choose` to select items to be deleted by numbers. User can input: * 1,5-7 : select 1,5,6,7 items to be deleted * * : select all items to be deleted * -* : unselect all, nothing will be deleted * : (empty) finish selecting, and return back to main menu Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/clean.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index 363690299c..643a5e0a01 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -680,6 +680,43 @@ static int filter_by_patterns_cmd(void) return 0; } +static int select_by_numbers_cmd(void) +{ + struct menu_opts menu_opts; + struct menu_stuff menu_stuff; + struct string_list_item *items; + int *chosen; + int i, j; + + menu_opts.header = NULL; + menu_opts.prompt = N_("Select items to delete"); + menu_opts.flags = 0; + + menu_stuff.type = MENU_STUFF_TYPE_STRING_LIST; + menu_stuff.stuff = &del_list; + menu_stuff.nr = del_list.nr; + + chosen = list_and_choose(&menu_opts, &menu_stuff); + items = del_list.items; + for (i = 0, j = 0; i < del_list.nr; i++) { + if (i < chosen[j]) { + *(items[i].string) = '\0'; + } else if (i == chosen[j]) { + /* delete selected item */ + j++; + continue; + } else { + /* end of chosen (chosen[j] == EOF), won't delete */ + *(items[i].string) = '\0'; + } + } + + string_list_remove_empty_items(&del_list, 0); + + free(chosen); + return 0; +} + static int quit_cmd(void) { string_list_clear(&del_list, 0); @@ -693,6 +730,7 @@ static int help_cmd(void) printf_ln(_( "clean - start cleaning\n" "filter by pattern - exclude items from deletion\n" + "select by numbers - select items to be deleted by numbers\n" "quit - stop cleaning\n" "help - this screen\n" "? - help for prompt selection" @@ -709,6 +747,7 @@ static void interactive_main_loop(void) struct menu_item menus[] = { {'c', "clean", 0, clean_cmd}, {'f', "filter by pattern", 0, filter_by_patterns_cmd}, + {'s', "select by numbers", 0, select_by_numbers_cmd}, {'q', "quit", 0, quit_cmd}, {'h', "help", 0, help_cmd}, }; From 96a799b6d1d4a63dccfe8b5dcabd2d738fa6c26e Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:54 +0800 Subject: [PATCH 13/16] git-clean: add ask each interactive action Add a new action for interactive git-clean: ask each. It's just like the "rm -i" command, that the user must confirm one by one for each file or directory to be cleaned. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- builtin/clean.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index 643a5e0a01..bf03acfe88 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -717,6 +717,40 @@ static int select_by_numbers_cmd(void) return 0; } +static int ask_each_cmd(void) +{ + struct strbuf confirm = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + const char *qname; + int changed = 0, eof = 0; + + for_each_string_list_item(item, &del_list) { + /* Ctrl-D should stop removing files */ + if (!eof) { + qname = quote_path_relative(item->string, NULL, &buf); + printf(_("remove %s? "), qname); + if (strbuf_getline(&confirm, stdin, '\n') != EOF) { + strbuf_trim(&confirm); + } else { + putchar('\n'); + eof = 1; + } + } + if (!confirm.len || strncasecmp(confirm.buf, "yes", confirm.len)) { + *item->string = '\0'; + changed++; + } + } + + if (changed) + string_list_remove_empty_items(&del_list, 0); + + strbuf_release(&buf); + strbuf_release(&confirm); + return MENU_RETURN_NO_LOOP; +} + static int quit_cmd(void) { string_list_clear(&del_list, 0); @@ -731,6 +765,7 @@ static int help_cmd(void) "clean - start cleaning\n" "filter by pattern - exclude items from deletion\n" "select by numbers - select items to be deleted by numbers\n" + "ask each - confirm each deletion (like \"rm -i\")\n" "quit - stop cleaning\n" "help - this screen\n" "? - help for prompt selection" @@ -748,6 +783,7 @@ static void interactive_main_loop(void) {'c', "clean", 0, clean_cmd}, {'f', "filter by pattern", 0, filter_by_patterns_cmd}, {'s', "select by numbers", 0, select_by_numbers_cmd}, + {'a', "ask each", 0, ask_each_cmd}, {'q', "quit", 0, quit_cmd}, {'h', "help", 0, help_cmd}, }; From c0be6b4c8a1d16a92efad00d73683075cf2da60d Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:55 +0800 Subject: [PATCH 14/16] git-clean: add documentation for interactive git-clean Add new section "Interactive mode" for documentation of interactive git-clean. Signed-off-by: Jiang Xin Helped-by: Eric Sunshine Signed-off-by: Junio C Hamano --- Documentation/git-clean.txt | 65 +++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/Documentation/git-clean.txt b/Documentation/git-clean.txt index 186e3455e4..75fb543393 100644 --- a/Documentation/git-clean.txt +++ b/Documentation/git-clean.txt @@ -39,8 +39,8 @@ OPTIONS -i:: --interactive:: - Show what would be done and the user must confirm before actually - cleaning. + Show what would be done and clean files interactively. See + ``Interactive mode'' for details. -n:: --dry-run:: @@ -69,6 +69,67 @@ OPTIONS Remove only files ignored by Git. This may be useful to rebuild everything from scratch, but keep manually created files. +Interactive mode +---------------- +When the command enters the interactive mode, it shows the +files and directories to be cleaned, and goes into its +interactive command loop. + +The command loop shows the list of subcommands available, and +gives a prompt "What now> ". In general, when the prompt ends +with a single '>', you can pick only one of the choices given +and type return, like this: + +------------ + *** Commands *** + 1: clean 2: filter by pattern 3: select by numbers + 4: ask each 5: quit 6: help + What now> 1 +------------ + +You also could say `c` or `clean` above as long as the choice is unique. + +The main command loop has 6 subcommands. + +clean:: + + Start cleaning files and directories, and then quit. + +filter by pattern:: + + This shows the files and directories to be deleted and issues an + "Input ignore patterns>>" prompt. You can input space-seperated + patterns to exclude files and directories from deletion. + E.g. "*.c *.h" will excludes files end with ".c" and ".h" from + deletion. When you are satisfied with the filtered result, press + ENTER (empty) back to the main menu. + +select by numbers:: + + This shows the files and directories to be deleted and issues an + "Select items to delete>>" prompt. When the prompt ends with double + '>>' like this, you can make more than one selection, concatenated + with whitespace or comma. Also you can say ranges. E.g. "2-5 7,9" + to choose 2,3,4,5,7,9 from the list. If the second number in a + range is omitted, all remaining patches are taken. E.g. "7-" to + choose 7,8,9 from the list. You can say '*' to choose everything. + Also when you are satisfied with the filtered result, press ENTER + (empty) back to the main menu. + +ask each:: + + This will start to clean, and you must confirm one by one in order + to delete items. Please note that this action is not as efficient + as the above two actions. + +quit:: + + This lets you quit without do cleaning. + +help:: + + Show brief usage of interactive git-clean. + SEE ALSO -------- linkgit:gitignore[5] From db627fd568410499c47d764937c3d7a10bbadffa Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:56 +0800 Subject: [PATCH 15/16] test: add t7301 for git-clean--interactive Add test cases for git-clean--interactive. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- t/t7301-clean-interactive.sh | 439 +++++++++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100755 t/t7301-clean-interactive.sh diff --git a/t/t7301-clean-interactive.sh b/t/t7301-clean-interactive.sh new file mode 100755 index 0000000000..4e6055d06a --- /dev/null +++ b/t/t7301-clean-interactive.sh @@ -0,0 +1,439 @@ +#!/bin/sh + +test_description='git clean -i basic tests' + +. ./test-lib.sh + +test_expect_success 'setup' ' + + mkdir -p src && + touch src/part1.c Makefile && + echo build >.gitignore && + echo \*.o >>.gitignore && + git add . && + git commit -m setup && + touch src/part2.c README && + git add . + +' + +test_expect_success 'git clean -i (clean)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + echo c | git clean -i && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test ! -f src/part3.h && + test ! -f src/part4.c && + test ! -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -i (quit)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + echo q | git clean -i && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -i (Ctrl+D)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + echo "\04" | git clean -i && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (filter all)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo f; echo "*"; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (filter patterns)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo f; echo "part3.* *.out"; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test ! -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test ! -f src/part4.c && + test ! -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (filter patterns 2)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo f; echo "* !*.out"; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - all)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo "*"; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test ! -f docs/manual.txt && + test ! -f src/part3.c && + test ! -f src/part3.h && + test ! -f src/part4.c && + test ! -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - none)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - number)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo 3; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - number 2)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo 2 3; echo 5; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test ! -f docs/manual.txt && + test ! -f src/part3.c && + test -f src/part3.h && + test ! -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - number 3)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo 3,4 5; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test ! -f src/part3.h && + test ! -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - range)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo 1,3-4; echo 2; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test ! -f src/part3.c && + test ! -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test ! -f docs/manual.txt && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (select - range 2)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo 4- 1; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test -f src/part3.c && + test ! -f src/part3.h && + test ! -f src/part4.c && + test ! -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (inverse select)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo s; echo "*"; echo -5- 1 -2; echo; echo c) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test ! -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (ask)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo a; echo Y; echo y; echo no; echo yes; echo bad; echo) | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test ! -f docs/manual.txt && + test -f src/part3.c && + test ! -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id (ask - Ctrl+D)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (echo a; echo Y; echo no; echo yes; echo "\04") | \ + git clean -id && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id with prefix and path (filter)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (cd build/ && \ + (echo f; echo "docs"; echo "*.h"; echo ; echo c) | \ + git clean -id ..) && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test -f docs/manual.txt && + test ! -f src/part3.c && + test -f src/part3.h && + test ! -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id with prefix and path (select by name)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (cd build/ && \ + (echo s; echo "../docs/"; echo "../src/part3.c"; \ + echo "../src/part4.c"; echo; echo c) | \ + git clean -id ..) && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test -f a.out && + test ! -f docs/manual.txt && + test ! -f src/part3.c && + test -f src/part3.h && + test ! -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_expect_success 'git clean -id with prefix and path (ask)' ' + + mkdir -p build docs && + touch a.out src/part3.c src/part3.h src/part4.c src/part4.h \ + docs/manual.txt obj.o build/lib.so && + (cd build/ && \ + (echo a; echo Y; echo y; echo no; echo yes; echo bad; echo) | \ + git clean -id ..) && + test -f Makefile && + test -f README && + test -f src/part1.c && + test -f src/part2.c && + test ! -f a.out && + test ! -f docs/manual.txt && + test -f src/part3.c && + test ! -f src/part3.h && + test -f src/part4.c && + test -f src/part4.h && + test -f obj.o && + test -f build/lib.so + +' + +test_done From abd4284bc62127a2db69c8c81501a56bb29284c8 Mon Sep 17 00:00:00 2001 From: Jiang Xin Date: Tue, 25 Jun 2013 23:53:57 +0800 Subject: [PATCH 16/16] test: run testcases with POSIX absolute paths on Windows Some test cases are skipped on Windows by marking with POSIX prereq. This is because arguments look like absolute paths (such as /a/b) for regular Windows programs (*.exe executables, no bash scripts) are changed to Windows paths (like C:/msysgit/a/b). There is no cygpath nor equivalent on msysGit, but it is easy to write one. New subcommand "mingw_path" is added in test-path-utils, so that we can get the expected absolute paths on Windows. E.g. COMMAND LINE Linux output Windows output ================================== ============ =============== test-path-utils mingw_path / / C:/msysgit test-path-utils mingw_path /a/b/ /a/b/ C:/msysgit/a/b/ With this utility, most skipped test cases in t0060 can be turned on to be tested correctly on Windows. Signed-off-by: Jiang Xin Signed-off-by: Junio C Hamano --- t/t0060-path-utils.sh | 44 ++++++++++++++++++++++--------------------- test-path-utils.c | 5 +++++ 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 76c779252c..3a48de20d8 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -8,13 +8,15 @@ test_description='Test various path utilities' . ./test-lib.sh norm_path() { + expected=$(test-path-utils mingw_path "$2") test_expect_success $3 "normalize path: $1 => $2" \ - "test \"\$(test-path-utils normalize_path_copy '$1')\" = '$2'" + "test \"\$(test-path-utils normalize_path_copy '$1')\" = '$expected'" } relative_path() { + expected=$(test-path-utils mingw_path "$3") test_expect_success $4 "relative path: $1 $2 => $3" \ - "test \"\$(test-path-utils relative_path '$1' '$2')\" = '$3'" + "test \"\$(test-path-utils relative_path '$1' '$2')\" = '$expected'" } # On Windows, we are using MSYS's bash, which mangles the paths. @@ -39,8 +41,8 @@ ancestor() { test \"\$actual\" = '$expected'" } -# Absolute path tests must be skipped on Windows because due to path mangling -# the test program never sees a POSIX-style absolute path +# Some absolute path tests should be skipped on Windows due to path mangling +# on POSIX-style absolute paths case $(uname -s) in *MINGW*) ;; @@ -73,30 +75,30 @@ norm_path d1/s1//../s2/../../d2 d2 norm_path d1/.../d2 d1/.../d2 norm_path d1/..././../d2 d1/d2 -norm_path / / POSIX +norm_path / / norm_path // / POSIX norm_path /// / POSIX -norm_path /. / POSIX +norm_path /. / norm_path /./ / POSIX norm_path /./.. ++failed++ POSIX -norm_path /../. ++failed++ POSIX +norm_path /../. ++failed++ norm_path /./../.// ++failed++ POSIX norm_path /dir/.. / POSIX norm_path /dir/sub/../.. / POSIX norm_path /dir/sub/../../.. ++failed++ POSIX -norm_path /dir /dir POSIX -norm_path /dir// /dir/ POSIX -norm_path /./dir /dir POSIX -norm_path /dir/. /dir/ POSIX -norm_path /dir///./ /dir/ POSIX -norm_path /dir//sub/.. /dir/ POSIX -norm_path /dir/sub/../ /dir/ POSIX +norm_path /dir /dir +norm_path /dir// /dir/ +norm_path /./dir /dir +norm_path /dir/. /dir/ +norm_path /dir///./ /dir/ +norm_path /dir//sub/.. /dir/ +norm_path /dir/sub/../ /dir/ norm_path //dir/sub/../. /dir/ POSIX -norm_path /dir/s1/../s2/ /dir/s2/ POSIX -norm_path /d1/s1///s2/..//../s3/ /d1/s3/ POSIX -norm_path /d1/s1//../s2/../../d2 /d2 POSIX -norm_path /d1/.../d2 /d1/.../d2 POSIX -norm_path /d1/..././../d2 /d1/d2 POSIX +norm_path /dir/s1/../s2/ /dir/s2/ +norm_path /d1/s1///s2/..//../s3/ /d1/s3/ +norm_path /d1/s1//../s2/../../d2 /d2 +norm_path /d1/.../d2 /d1/.../d2 +norm_path /d1/..././../d2 /d1/d2 ancestor / / -1 ancestor /foo / 0 @@ -198,8 +200,8 @@ relative_path / /a/b/ ../../ relative_path /a/c /a/b/ ../c relative_path /a/c /a/b ../c relative_path /x/y /a/b/ ../../x/y -relative_path /a/b "" /a/b POSIX -relative_path /a/b "" /a/b POSIX +relative_path /a/b "" /a/b +relative_path /a/b "" /a/b relative_path a/b/c/ a/b/ c/ relative_path a/b/c/ a/b c/ relative_path a/b//c a//b c diff --git a/test-path-utils.c b/test-path-utils.c index 1bf4730619..bb975e4d3e 100644 --- a/test-path-utils.c +++ b/test-path-utils.c @@ -116,6 +116,11 @@ int main(int argc, char **argv) return 0; } + if (argc == 3 && !strcmp(argv[1], "mingw_path")) { + puts(argv[2]); + return 0; + } + if (argc == 4 && !strcmp(argv[1], "relative_path")) { struct strbuf sb = STRBUF_INIT; const char *in, *prefix, *rel;