Skip to content

Commit

Permalink
patch in sprase-checkout from libgit2#6394
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenhz authored and Pawel Czarnecki committed Mar 8, 2023
1 parent 9535860 commit 855ed0c
Show file tree
Hide file tree
Showing 93 changed files with 3,299 additions and 177 deletions.
1 change: 1 addition & 0 deletions include/git2.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include "git2/revparse.h"
#include "git2/revwalk.h"
#include "git2/signature.h"
#include "git2/sparse.h"
#include "git2/stash.h"
#include "git2/status.h"
#include "git2/submodule.h"
Expand Down
6 changes: 6 additions & 0 deletions include/git2/checkout.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ typedef enum {
/** Include common ancestor data in zdiff3 format for conflicts */
GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3 = (1u << 25),

/**
* Remove files that are excluded by the sparse-checkout ruleset.
* Does nothing when GIT_CHECKOUT_SAFE is set.
*/
GIT_CHECKOUT_REMOVE_SPARSE_FILES = (1u << 26),

/**
* THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED
*/
Expand Down
5 changes: 5 additions & 0 deletions include/git2/diff.h
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,11 @@ typedef struct {
* Defaults to "b".
*/
const char *new_prefix;

/** Skip files in the diff that are excluded by the `sparse-checkout` file.
* Set to 1 to skip sparse files, 0 otherwise
*/
int skip_sparse_files;
} git_diff_options;

/* The current version of the diff options structure */
Expand Down
114 changes: 114 additions & 0 deletions include/git2/sparse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* 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.
*/
#ifndef INCLUDE_git_sparse_h__
#define INCLUDE_git_sparse_h__

#include "common.h"
#include "types.h"

GIT_BEGIN_DECL

typedef struct {
unsigned int version; /**< The version */

/**
* Set to zero (false) to consider sparse-checkout patterns as
* full patterns, or non-zero for cone patterns.
*/
/* int cone; */
} git_sparse_checkout_init_options;

#define GIT_SPARSE_CHECKOUT_INIT_OPTIONS_VERSION 1
#define GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT {GIT_SPARSE_CHECKOUT_INIT_OPTIONS_VERSION};

/**
* Enable the core.sparseCheckout setting. If the sparse-checkout
* file does not exist, then populate it with patterns that match
* every file in the root directory and no other directories.
*
* @param repo Repository where to find the sparse-checkout file
* @param opts The `git_sparse_checkout_init_options` when
* initializing the sparse-checkout file
* @return 0 or an error code
*/
GIT_EXTERN(int) git_sparse_checkout_init(
git_repository *repo,
git_sparse_checkout_init_options *opts);

/**
* Fill a list with all the patterns in the sparse-checkout file
*
* @param patterns Pointer to a git_strarray structure where
* the patterns will be stored
* @param repo Repository where to find the sparse-checkout file
* @return 0 or an error code
*/
GIT_EXTERN(int) git_sparse_checkout_list(
git_strarray *patterns,
git_repository *repo);

/**
* Write a set of patterns to the sparse-checkout file.
* Update the working directory to match the new patterns.
* Enable the core.sparseCheckout config setting if it is not
* already enabled.
*
* @param repo Repository where to find the sparse-checkout file
* @param patterns Pointer to a git_strarray structure where
* the patterns to set can be found
* @return 0 or an error code
*/
GIT_EXTERN(int) git_sparse_checkout_set(
git_repository *repo,
git_strarray *patterns);

/**
* Update the sparse-checkout file to include additional patterns.
*
* @param repo Repository where to find the sparse-checkout file
* @param patterns Pointer to a git_strarray structure where
* the patterns to set can be found
* @return 0 or an error code
*/
GIT_EXTERN(int) git_sparse_checkout_add(
git_repository *repo,
git_strarray *patterns);

GIT_EXTERN(int) git_sparse_checkout_reapply(git_repository *repo);

/**
* Disable the core.sparseCheckout config setting, and restore the
* working directory to include all files. Leaves the sparse-checkout
* file intact so a later git sparse-checkout init command may return
* the working directory to the same state.
*
* @param repo Repository where to find the sparse-checkout file
* @return 0 or an error code
*/
GIT_EXTERN(int) git_sparse_checkout_disable(git_repository *repo);

/**
* Test if the sparse-checkout rules apply to a given path.
*
* This function checks the sparse-checkout rules to see if they would apply
* to the given path. This indicates if the path would be included on checkout.
*
* @param checkout boolean returning 1 if the sparse-checkout rules apply
* (the file will be checked out), 0 if they do not
* @param repo Repository where to find the sparse-checkout file
* @param path the path to check sparse-checkout rules for, relative to the repo's workdir.
* @return 0 if sparse-checkout rules could be processed for the path
* (regardless of whether it exists or not), or an error < 0 if they could not.
*/
GIT_EXTERN(int) git_sparse_check_path(
int *checkout,
git_repository *repo,
const char *path);

GIT_END_DECL

#endif
149 changes: 149 additions & 0 deletions src/libgit2/attr_file.c
Original file line number Diff line number Diff line change
Expand Up @@ -1025,3 +1025,152 @@ void git_attr_session__free(git_attr_session *session)

memset(session, 0, sizeof(git_attr_session));
}


/**
* A negative ignore pattern can negate a positive one without
* wildcards if it is a basename only and equals the basename of
* the positive pattern. Thus
*
* foo/bar
* !bar
*
* would result in foo/bar being unignored again while
*
* moo/foo/bar
* !foo/bar
*
* would do nothing. The reverse also holds true: a positive
* basename pattern can be negated by unignoring the basename in
* subdirectories. Thus
*
* bar
* !foo/bar
*
* would result in foo/bar being unignored again. As with the
* first case,
*
* foo/bar
* !moo/foo/bar
*
* would do nothing, again.
*/
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
{
int (*cmp)(const char *, const char *, size_t);
git_attr_fnmatch *longer, *shorter;
char *p;

if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
|| (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
return false;

if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
cmp = git__strncasecmp;
else
cmp = git__strncmp;

/* If lengths match we need to have an exact match */
if (rule->length == neg->length) {
return cmp(rule->pattern, neg->pattern, rule->length) == 0;
} else if (rule->length < neg->length) {
shorter = rule;
longer = neg;
} else {
shorter = neg;
longer = rule;
}

/* Otherwise, we need to check if the shorter
* rule is a basename only (that is, it contains
* no path separator) and, if so, if it
* matches the tail of the longer rule */
p = longer->pattern + longer->length - shorter->length;

if (p[-1] != '/')
return false;
if (memchr(shorter->pattern, '/', shorter->length) != NULL)
return false;

return cmp(p, shorter->pattern, shorter->length) == 0;
}

/**
* A negative ignore can only unignore a file which is given explicitly before, thus
*
* foo
* !foo/bar
*
* does not unignore 'foo/bar' as it's not in the list. However
*
* foo/<star>
* !foo/bar
*
* does unignore 'foo/bar', as it is contained within the 'foo/<star>' rule.
*/
int git_attr__does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
{
int error = 0, wildmatch_flags, effective_flags;
size_t i;
git_attr_fnmatch *rule;
char *path;
git_str buf = GIT_STR_INIT;

*out = 0;

wildmatch_flags = WM_PATHNAME;
if (match->flags & GIT_ATTR_FNMATCH_ICASE)
wildmatch_flags |= WM_CASEFOLD;

/* path of the file relative to the workdir, so we match the rules in subdirs */
if (match->containing_dir) {
git_str_puts(&buf, match->containing_dir);
}
if (git_str_puts(&buf, match->pattern) < 0)
return -1;

path = git_str_detach(&buf);

git_vector_foreach(rules, i, rule) {
if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
if (does_negate_pattern(rule, match)) {
error = 0;
*out = 1;
goto out;
}
else
continue;
}

git_str_clear(&buf);
if (rule->containing_dir)
git_str_puts(&buf, rule->containing_dir);
git_str_puts(&buf, rule->pattern);

if (git_str_oom(&buf))
goto out;

/*
* if rule isn't for full path we match without PATHNAME flag
* as lines like *.txt should match something like dir/test.txt
* requiring * to also match /
*/
effective_flags = wildmatch_flags;
if (!(rule->flags & GIT_ATTR_FNMATCH_FULLPATH))
effective_flags &= ~WM_PATHNAME;

/* if we found a match, we want to keep this rule */
if ((wildmatch(git_str_cstr(&buf), path, effective_flags)) == WM_MATCH) {
*out = 1;
error = 0;
goto out;
}
}

error = 0;

out:
git__free(path);
git_str_dispose(&buf);
return error;
}
2 changes: 2 additions & 0 deletions src/libgit2/attr_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,6 @@ extern int git_attr_assignment__parse(
git_vector *assigns,
const char **scan);

extern int git_attr__does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match);

#endif
13 changes: 11 additions & 2 deletions src/libgit2/checkout.c
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,15 @@ static int checkout_action_wd_only(

if (wd->mode != GIT_FILEMODE_TREE) {
if (!error) { /* found by git_index__find_pos call */
notify = GIT_CHECKOUT_NOTIFY_DIRTY;
remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0);

/* Sparse checkout will set the SKIP_WORKTREE bit if a file should be skipped */
const git_index_entry *e = git_index_get_byindex(data->index, pos);
if (e == NULL ||
(e->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE) == 0 ||
(data->strategy & GIT_CHECKOUT_REMOVE_SPARSE_FILES) != 0) {
notify = GIT_CHECKOUT_NOTIFY_DIRTY;
remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0);
}
} else if (error != GIT_ENOTFOUND)
return error;
else
Expand Down Expand Up @@ -2571,6 +2578,8 @@ int git_checkout_iterator(
GIT_DIFF_INCLUDE_TYPECHANGE_TREES |
GIT_DIFF_SKIP_BINARY_CHECK |
GIT_DIFF_INCLUDE_CASECHANGE;
diff_opts.skip_sparse_files = 1;

if (data.opts.checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH)
diff_opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH;
if (data.opts.paths.count > 0)
Expand Down
1 change: 1 addition & 0 deletions src/libgit2/config_cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ static struct map_data _configmaps[] = {
{"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT },
{"core.fsyncobjectfiles", NULL, 0, GIT_FSYNCOBJECTFILES_DEFAULT },
{"core.longpaths", NULL, 0, GIT_LONGPATHS_DEFAULT },
{"core.sparsecheckout", NULL, 0, GIT_SPARSECHECKOUT_DEFAULT },
};

int git_config__configmap_lookup(int *out, git_config *config, git_configmap_item item)
Expand Down
18 changes: 17 additions & 1 deletion src/libgit2/diff_generate.c
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,12 @@ static int maybe_modified(
if (!diff_pathspec_match(&matched_pathspec, diff, oitem))
return 0;

if (diff->base.opts.skip_sparse_files &&
git_iterator_current_skip_checkout(info->new_iter))
return 0;

memset(&noid, 0, sizeof(noid));

/* on platforms with no symlinks, preserve mode of existing symlinks */
if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir &&
!(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS))
Expand Down Expand Up @@ -1028,6 +1034,11 @@ static int handle_unmatched_new_item(
const git_index_entry *nitem = info->nitem;
git_delta_t delta_type = GIT_DELTA_UNTRACKED;
bool contains_oitem;

/* check if this item should be skipped due to sparse checkout */
if (diff->base.opts.skip_sparse_files &&
git_iterator_current_skip_checkout(info->new_iter))
return iterator_advance(&info->nitem, info->new_iter);

/* check if this is a prefix of the other side */
contains_oitem = entry_is_prefixed(diff, info->oitem, nitem);
Expand Down Expand Up @@ -1188,7 +1199,12 @@ static int handle_unmatched_old_item(
if (git_index_entry_is_conflict(info->oitem))
delta_type = GIT_DELTA_CONFLICTED;

if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0)
if ((diff->base.opts.skip_sparse_files &&
git_iterator_current_skip_checkout(info->new_iter)) ||
(info->oitem->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE) != 0)
delta_type = GIT_DELTA_UNMODIFIED;

else if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0)
return error;

/* if we are generating TYPECHANGE records then check for that
Expand Down

0 comments on commit 855ed0c

Please sign in to comment.