mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-07 23:54:50 +00:00

The git_status_file API was doing a hack to deal with files that are inside ignored directories. The status scan was not reporting any file in this case, so git_status_file would attempt a final "stat()" call, and return IGNORED if the file actually existed. On case-insensitive filesystems where core.ignorecase is set incorrectly, this magic check can "succeed" and report a file as ignored when it should actually return ENOTFOUND. Now that we have the GIT_STATUS_OPT_RECURSE_IGNORED_DIRS, we can use that flag to make sure that git_status_file() will look into ignored directories and eliminate the hack completely, so we give the correct error.
305 lines
7.5 KiB
C
305 lines
7.5 KiB
C
/*
|
|
* Copyright (C) the libgit2 contributors. All rights reserved.
|
|
*
|
|
* This file is part of libgit2, distributed under the GNU GPL v2 with
|
|
* a Linking Exception. For full terms see the included COPYING file.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "git2.h"
|
|
#include "fileops.h"
|
|
#include "hash.h"
|
|
#include "vector.h"
|
|
#include "tree.h"
|
|
#include "git2/status.h"
|
|
#include "repository.h"
|
|
#include "ignore.h"
|
|
|
|
#include "git2/diff.h"
|
|
#include "diff.h"
|
|
#include "diff_output.h"
|
|
|
|
static unsigned int index_delta2status(git_delta_t index_status)
|
|
{
|
|
unsigned int st = GIT_STATUS_CURRENT;
|
|
|
|
switch (index_status) {
|
|
case GIT_DELTA_ADDED:
|
|
case GIT_DELTA_COPIED:
|
|
st = GIT_STATUS_INDEX_NEW;
|
|
break;
|
|
case GIT_DELTA_DELETED:
|
|
st = GIT_STATUS_INDEX_DELETED;
|
|
break;
|
|
case GIT_DELTA_MODIFIED:
|
|
st = GIT_STATUS_INDEX_MODIFIED;
|
|
break;
|
|
case GIT_DELTA_RENAMED:
|
|
st = GIT_STATUS_INDEX_RENAMED;
|
|
break;
|
|
case GIT_DELTA_TYPECHANGE:
|
|
st = GIT_STATUS_INDEX_TYPECHANGE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return st;
|
|
}
|
|
|
|
static unsigned int workdir_delta2status(git_delta_t workdir_status)
|
|
{
|
|
unsigned int st = GIT_STATUS_CURRENT;
|
|
|
|
switch (workdir_status) {
|
|
case GIT_DELTA_ADDED:
|
|
case GIT_DELTA_RENAMED:
|
|
case GIT_DELTA_COPIED:
|
|
case GIT_DELTA_UNTRACKED:
|
|
st = GIT_STATUS_WT_NEW;
|
|
break;
|
|
case GIT_DELTA_DELETED:
|
|
st = GIT_STATUS_WT_DELETED;
|
|
break;
|
|
case GIT_DELTA_MODIFIED:
|
|
st = GIT_STATUS_WT_MODIFIED;
|
|
break;
|
|
case GIT_DELTA_IGNORED:
|
|
st = GIT_STATUS_IGNORED;
|
|
break;
|
|
case GIT_DELTA_TYPECHANGE:
|
|
st = GIT_STATUS_WT_TYPECHANGE;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return st;
|
|
}
|
|
|
|
typedef struct {
|
|
git_status_cb cb;
|
|
void *payload;
|
|
const git_status_options *opts;
|
|
} status_user_callback;
|
|
|
|
static int status_invoke_cb(
|
|
git_diff_delta *h2i, git_diff_delta *i2w, void *payload)
|
|
{
|
|
status_user_callback *usercb = payload;
|
|
const char *path = NULL;
|
|
unsigned int status = 0;
|
|
|
|
if (i2w) {
|
|
path = i2w->old_file.path;
|
|
status |= workdir_delta2status(i2w->status);
|
|
}
|
|
if (h2i) {
|
|
path = h2i->old_file.path;
|
|
status |= index_delta2status(h2i->status);
|
|
}
|
|
|
|
/* if excluding submodules and this is a submodule everywhere */
|
|
if (usercb->opts &&
|
|
(usercb->opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
|
|
{
|
|
bool in_tree = (h2i && h2i->status != GIT_DELTA_ADDED);
|
|
bool in_index = (h2i && h2i->status != GIT_DELTA_DELETED);
|
|
bool in_wd = (i2w && i2w->status != GIT_DELTA_DELETED);
|
|
|
|
if ((!in_tree || h2i->old_file.mode == GIT_FILEMODE_COMMIT) &&
|
|
(!in_index || h2i->new_file.mode == GIT_FILEMODE_COMMIT) &&
|
|
(!in_wd || i2w->new_file.mode == GIT_FILEMODE_COMMIT))
|
|
return 0;
|
|
}
|
|
|
|
return usercb->cb(path, status, usercb->payload);
|
|
}
|
|
|
|
int git_status_foreach_ext(
|
|
git_repository *repo,
|
|
const git_status_options *opts,
|
|
git_status_cb cb,
|
|
void *payload)
|
|
{
|
|
int err = 0;
|
|
git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT;
|
|
git_diff_list *head2idx = NULL, *idx2wd = NULL;
|
|
git_tree *head = NULL;
|
|
git_status_show_t show =
|
|
opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
|
|
status_user_callback usercb;
|
|
|
|
assert(show <= GIT_STATUS_SHOW_INDEX_THEN_WORKDIR);
|
|
|
|
GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");
|
|
|
|
if (show != GIT_STATUS_SHOW_INDEX_ONLY &&
|
|
(err = git_repository__ensure_not_bare(repo, "status")) < 0)
|
|
return err;
|
|
|
|
/* if there is no HEAD, that's okay - we'll make an empty iterator */
|
|
if (((err = git_repository_head_tree(&head, repo)) < 0) &&
|
|
!(err == GIT_ENOTFOUND || err == GIT_EORPHANEDHEAD))
|
|
return err;
|
|
|
|
memcpy(&diffopt.pathspec, &opts->pathspec, sizeof(diffopt.pathspec));
|
|
|
|
diffopt.flags = GIT_DIFF_INCLUDE_TYPECHANGE;
|
|
|
|
if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNTRACKED) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNTRACKED;
|
|
if ((opts->flags & GIT_STATUS_OPT_INCLUDE_IGNORED) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_IGNORED;
|
|
if ((opts->flags & GIT_STATUS_OPT_INCLUDE_UNMODIFIED) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_INCLUDE_UNMODIFIED;
|
|
if ((opts->flags & GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
|
|
if ((opts->flags & GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_DISABLE_PATHSPEC_MATCH;
|
|
if ((opts->flags & GIT_STATUS_OPT_RECURSE_IGNORED_DIRS) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_RECURSE_IGNORED_DIRS;
|
|
if ((opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
|
|
diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES;
|
|
|
|
if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
|
|
err = git_diff_tree_to_index(&head2idx, repo, head, NULL, &diffopt);
|
|
if (err < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
|
|
err = git_diff_index_to_workdir(&idx2wd, repo, NULL, &diffopt);
|
|
if (err < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
usercb.cb = cb;
|
|
usercb.payload = payload;
|
|
usercb.opts = opts;
|
|
|
|
if (show == GIT_STATUS_SHOW_INDEX_THEN_WORKDIR) {
|
|
if ((err = git_diff__paired_foreach(
|
|
head2idx, NULL, status_invoke_cb, &usercb)) < 0)
|
|
goto cleanup;
|
|
|
|
git_diff_list_free(head2idx);
|
|
head2idx = NULL;
|
|
}
|
|
|
|
err = git_diff__paired_foreach(head2idx, idx2wd, status_invoke_cb, &usercb);
|
|
|
|
cleanup:
|
|
git_tree_free(head);
|
|
git_diff_list_free(head2idx);
|
|
git_diff_list_free(idx2wd);
|
|
|
|
if (err == GIT_EUSER)
|
|
giterr_clear();
|
|
|
|
return err;
|
|
}
|
|
|
|
int git_status_foreach(
|
|
git_repository *repo,
|
|
git_status_cb callback,
|
|
void *payload)
|
|
{
|
|
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
|
|
|
|
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
|
|
opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
|
|
GIT_STATUS_OPT_INCLUDE_UNTRACKED |
|
|
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
|
|
|
|
return git_status_foreach_ext(repo, &opts, callback, payload);
|
|
}
|
|
|
|
struct status_file_info {
|
|
char *expected;
|
|
unsigned int count;
|
|
unsigned int status;
|
|
int fnm_flags;
|
|
int ambiguous;
|
|
};
|
|
|
|
static int get_one_status(const char *path, unsigned int status, void *data)
|
|
{
|
|
struct status_file_info *sfi = data;
|
|
int (*strcomp)(const char *a, const char *b);
|
|
|
|
sfi->count++;
|
|
sfi->status = status;
|
|
|
|
strcomp = (sfi->fnm_flags & FNM_CASEFOLD) ? git__strcasecmp : git__strcmp;
|
|
|
|
if (sfi->count > 1 ||
|
|
(strcomp(sfi->expected, path) != 0 &&
|
|
p_fnmatch(sfi->expected, path, sfi->fnm_flags) != 0))
|
|
{
|
|
sfi->ambiguous = true;
|
|
return GIT_EAMBIGUOUS; /* giterr_set will be done by caller */
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int git_status_file(
|
|
unsigned int *status_flags,
|
|
git_repository *repo,
|
|
const char *path)
|
|
{
|
|
int error;
|
|
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
|
|
struct status_file_info sfi = {0};
|
|
git_index *index;
|
|
|
|
assert(status_flags && repo && path);
|
|
|
|
if ((error = git_repository_index__weakptr(&index, repo)) < 0)
|
|
return error;
|
|
|
|
if ((sfi.expected = git__strdup(path)) == NULL)
|
|
return -1;
|
|
if (index->ignore_case)
|
|
sfi.fnm_flags = FNM_CASEFOLD;
|
|
|
|
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
|
|
opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED |
|
|
GIT_STATUS_OPT_RECURSE_IGNORED_DIRS |
|
|
GIT_STATUS_OPT_INCLUDE_UNTRACKED |
|
|
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS |
|
|
GIT_STATUS_OPT_INCLUDE_UNMODIFIED;
|
|
opts.pathspec.count = 1;
|
|
opts.pathspec.strings = &sfi.expected;
|
|
|
|
error = git_status_foreach_ext(repo, &opts, get_one_status, &sfi);
|
|
|
|
if (error < 0 && sfi.ambiguous) {
|
|
giterr_set(GITERR_INVALID,
|
|
"Ambiguous path '%s' given to git_status_file", sfi.expected);
|
|
error = GIT_EAMBIGUOUS;
|
|
}
|
|
|
|
if (!error && !sfi.count) {
|
|
giterr_set(GITERR_INVALID,
|
|
"Attempt to get status of nonexistent file '%s'", path);
|
|
error = GIT_ENOTFOUND;
|
|
}
|
|
|
|
*status_flags = sfi.status;
|
|
|
|
git__free(sfi.expected);
|
|
|
|
return error;
|
|
}
|
|
|
|
int git_status_should_ignore(
|
|
int *ignored,
|
|
git_repository *repo,
|
|
const char *path)
|
|
{
|
|
return git_ignore_path_is_ignored(ignored, repo, path);
|
|
}
|
|
|