commit bd8de4305a32b69e3b4d44c9785663889a5d9eff from: Stefan Sperling date: Fri Oct 04 12:51:33 2019 UTC make 'got status' read .gitignore files; support **/ and /**/ in patterns commit - db1d3576eb40d1c48c15fd8d531e030296874e9f commit + bd8de4305a32b69e3b4d44c9785663889a5d9eff blob - 263cc8d4745cef602cb6a35a07c30ca7ebb6a9a1 blob + 87b6cffd29ca3fa2f2dbb432731c506f690c7afe --- got/got.1 +++ got/got.1 @@ -279,20 +279,37 @@ Changes created on top of staged changes are indicated .El .Pp For compatibility with -.Xr cvs 1 , +.Xr cvs 1 +and +.Xr git 1 , .Cm got status -parses +reads +.Xr glob 7 +patterns from .Pa .cvsignore +and +.Pa .gitignore files in each traversed directory and will not display unversioned files -which match +which match these patterns. +As an extension to .Xr glob 7 -ignore patterns contained in -.Pa .cvsignore -files. +matching rules, +.Cm got status +supports consecutive asterisks, +.Dq ** , +which will match an arbitrary amount of directories. Unlike .Xr cvs 1 , .Cm got status only supports a single ignore pattern per line. +Unlike +.Xr git 1 , +.Cm got status +does not support negated ignore patterns prefixed with +.Dq \&! , +and gives no special significance to the location of path component separators, +.Dq / , +in a pattern. .It Cm st Short alias for .Cm status . blob - 729abd35f59472f4970e7b7f254344f6af2e1b79 blob + d7dc1224936d7d6588be61fac9a25737e3bc8965 --- lib/worktree.c +++ lib/worktree.c @@ -2382,6 +2382,15 @@ read_ignores(struct got_pathlist_head *ignores, const while ((linelen = getline(&line, &linesize, f)) != -1) { if (linelen > 0 && line[linelen - 1] == '\n') line[linelen - 1] = '\0'; + + /* Git's ignores may contain comments. */ + if (line[0] == '#') + continue; + + /* Git's negated patterns are not (yet?) supported. */ + if (line[0] == '!') + continue; + if (asprintf(&pattern, "%s%s%s", path, path[0] ? "/" : "", line) == -1) { err = got_error_from_errno("asprintf"); @@ -2415,7 +2424,34 @@ int match_ignores(struct got_pathlist_head *ignores, const char *path) { struct got_pathlist_entry *pe; + + /* Handle patterns which match in all directories. */ + TAILQ_FOREACH(pe, ignores, entry) { + struct got_pathlist_head *ignorelist = pe->data; + struct got_pathlist_entry *pi; + + TAILQ_FOREACH(pi, ignorelist, entry) { + const char *p, *pattern = pi->path; + if (strncmp(pattern, "**/", 3) != 0) + continue; + pattern += 3; + p = path; + while (*p) { + if (fnmatch(pattern, p, + FNM_PATHNAME | FNM_LEADING_DIR)) { + /* Retry in next directory. */ + while (*p && *p != '/') + p++; + while (*p == '/') + p++; + continue; + } + return 1; + } + } + } + /* * The ignores pathlist contains ignore lists from children before * parents, so we can find the most specific ignorelist by walking @@ -2427,8 +2463,11 @@ match_ignores(struct got_pathlist_head *ignores, const struct got_pathlist_head *ignorelist = pe->data; struct got_pathlist_entry *pi; TAILQ_FOREACH(pi, ignorelist, entry) { - if (fnmatch(pi->path, path, - FNM_PATHNAME | FNM_LEADING_DIR)) + const char *pattern = pi->path; + int flags = FNM_LEADING_DIR; + if (strstr(pattern, "/**/") == NULL) + flags |= FNM_PATHNAME; + if (fnmatch(pattern, path, flags)) continue; return 1; } @@ -2441,15 +2480,14 @@ match_ignores(struct got_pathlist_head *ignores, const static const struct got_error * add_ignores(struct got_pathlist_head *ignores, const char *root_path, - const char *path) + const char *path, const char *ignores_filename) { const struct got_error *err = NULL; char *ignorespath; FILE *ignoresfile = NULL; - /* TODO: read .gitignores as well... */ - if (asprintf(&ignorespath, "%s/%s%s.cvsignore", root_path, path, - path[0] ? "/" : "") == -1) + if (asprintf(&ignorespath, "%s/%s%s%s", root_path, path, + path[0] ? "/" : "", ignores_filename) == -1) return got_error_from_errno("asprintf"); ignoresfile = fopen(ignorespath, "r"); @@ -2487,8 +2525,13 @@ status_new(void *arg, struct dirent *de, const char *p path = de->d_name; } - if (de->d_type == DT_DIR) - err = add_ignores(&a->ignores, a->worktree->root_path, path); + if (de->d_type == DT_DIR) { + err = add_ignores(&a->ignores, a->worktree->root_path, path, + ".cvsignore"); + if (err == NULL) + err = add_ignores(&a->ignores, a->worktree->root_path, + path, ".gitignore"); + } else if (got_path_is_child(path, a->status_path, a->status_path_len) && !match_ignores(&a->ignores, path)) err = (*a->status_cb)(a->status_arg, GOT_STATUS_UNVERSIONED, @@ -2563,8 +2606,12 @@ worktree_status(struct got_worktree *worktree, const c arg.cancel_cb = cancel_cb; arg.cancel_arg = cancel_arg; TAILQ_INIT(&arg.ignores); - err = add_ignores(&arg.ignores, worktree->root_path, path); + err = add_ignores(&arg.ignores, worktree->root_path, path, + ".cvsignore"); if (err == NULL) + err = add_ignores(&arg.ignores, worktree->root_path, + path, ".gitignore"); + if (err == NULL) err = got_fileindex_diff_dir(fileindex, workdir, worktree->root_path, path, repo, &fdiff_cb, &arg); free_ignores(&arg.ignores); blob - 5fde224adeff7a5c44b9167d6ceb971c1493e503 blob + c03418c5b2c40e6b760d26e575fb2febcd53ff37 --- regress/cmdline/status.sh +++ regress/cmdline/status.sh @@ -519,7 +519,56 @@ function test_status_cvsignore { echo '? .cvsignore' > $testroot/stdout.expected echo '? epsilon/.cvsignore' >> $testroot/stdout.expected echo '? epsilon/boo' >> $testroot/stdout.expected + echo '? foop' >> $testroot/stdout.expected + (cd $testroot/wt/gamma && got status > $testroot/stdout) + + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + fi + test_done "$testroot" "$ret" +} + +function test_status_gitignore { + local testroot=`test_init status_gitignore` + + got checkout $testroot/repo $testroot/wt > /dev/null + ret="$?" + if [ "$ret" != "0" ]; then + test_done "$testroot" "$ret" + return 1 + fi + + echo "unversioned file" > $testroot/wt/foo + echo "unversioned file" > $testroot/wt/foop + echo "unversioned file" > $testroot/wt/barp + echo "unversioned file" > $testroot/wt/epsilon/bar + echo "unversioned file" > $testroot/wt/epsilon/boo + echo "unversioned file" > $testroot/wt/epsilon/moo + mkdir -p $testroot/wt/a/b/c/ + echo "unversioned file" > $testroot/wt/a/b/c/foo + echo "unversioned file" > $testroot/wt/a/b/c/zoo + echo "foo" > $testroot/wt/.gitignore + echo "bar*" >> $testroot/wt/.gitignore + echo "epsilon/**" >> $testroot/wt/.gitignore + echo "a/**/foo" >> $testroot/wt/.gitignore + echo "**/zoo" >> $testroot/wt/.gitignore + + echo '? .gitignore' > $testroot/stdout.expected echo '? foop' >> $testroot/stdout.expected + (cd $testroot/wt && got status > $testroot/stdout) + + cmp -s $testroot/stdout.expected $testroot/stdout + ret="$?" + if [ "$ret" != "0" ]; then + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + echo '? .gitignore' > $testroot/stdout.expected + echo '? foop' >> $testroot/stdout.expected (cd $testroot/wt/gamma && got status > $testroot/stdout) cmp -s $testroot/stdout.expected $testroot/stdout @@ -543,3 +592,4 @@ run_test test_status_empty_dir run_test test_status_empty_dir_unversioned_file run_test test_status_many_paths run_test test_status_cvsignore +run_test test_status_gitignore