diff --git a/include/git2/checkout.h b/include/git2/checkout.h index deb828722..42d47003d 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -21,21 +21,30 @@ */ GIT_BEGIN_DECL - -#define GIT_CHECKOUT_OVERWRITE_EXISTING 0 /* default */ -#define GIT_CHECKOUT_SKIP_EXISTING 1 +enum { + GIT_CHECKOUT_DEFAULT = (1 << 0), + GIT_CHECKOUT_OVERWRITE_MODIFIED = (1 << 1), + GIT_CHECKOUT_CREATE_MISSING = (1 << 2), + GIT_CHECKOUT_REMOVE_UNTRACKED = (1 << 3), +}; /* Use zeros to indicate default settings */ typedef struct git_checkout_opts { - int existing_file_action; /* default: GIT_CHECKOUT_OVERWRITE_EXISTING */ + unsigned int checkout_strategy; /* default: GIT_CHECKOUT_DEFAULT */ int disable_filters; int dir_mode; /* default is 0755 */ int file_mode; /* default is 0644 */ int file_open_flags; /* default is O_CREAT | O_TRUNC | O_WRONLY */ + + /* when not NULL, arrays of fnmatch pattern specifying + * which paths should be taken into account + */ + git_strarray paths; } git_checkout_opts; /** - * Updates files in the working tree to match the commit pointed to by HEAD. + * Updates files in the index and the working tree to match the content of the + * commit pointed at by HEAD. * * @param repo repository to check out (must be non-bare) * @param opts specifies checkout options (may be NULL) @@ -49,19 +58,36 @@ GIT_EXTERN(int) git_checkout_head( git_indexer_stats *stats); /** - * Updates files in the working tree to match a commit pointed to by a ref. + * Updates files in the working tree to match the content of the index. * - * @param ref reference to follow to a commit + * @param repo repository to check out (must be non-bare) * @param opts specifies checkout options (may be NULL) * @param stats structure through which progress information is reported * @return 0 on success, GIT_ERROR otherwise (use giterr_last for information * about the error) */ -GIT_EXTERN(int) git_checkout_reference( - git_reference *ref, +GIT_EXTERN(int) git_checkout_index( + git_repository *repo, git_checkout_opts *opts, git_indexer_stats *stats); +/** + * Updates files in the index and working tree to match the content of the + * tree pointed at by the treeish. + * + * @param repo repository to check out (must be non-bare) + * @param treeish a commit, tag or tree which content will be used to update + * the working directory + * @param opts specifies checkout options (may be NULL) + * @param stats structure through which progress information is reported + * @return 0 on success, GIT_ERROR otherwise (use giterr_last for information + * about the error) + */ +GIT_EXTERN(int) git_checkout_tree( + git_repository *repo, + git_object *treeish, + git_checkout_opts *opts, + git_indexer_stats *stats); /** @} */ GIT_END_DECL diff --git a/include/git2/repository.h b/include/git2/repository.h index 32ec58dae..025a0a95d 100644 --- a/include/git2/repository.h +++ b/include/git2/repository.h @@ -456,7 +456,7 @@ GIT_EXTERN(int) git_repository_index(git_index **out, git_repository *repo); GIT_EXTERN(void) git_repository_set_index(git_repository *repo, git_index *index); /** - * Retrive git's prepared message + * Retrieve git's prepared message * * Operations such as git revert/cherry-pick/merge with the -n option * stop just short of creating a commit with the changes and save @@ -506,6 +506,67 @@ GIT_EXTERN(int) git_repository_hashfile( git_otype type, const char *as_path); +/** + * Make the repository HEAD point to the specified reference. + * + * If the provided reference points to a Tree or a Blob, the HEAD is + * unaltered and -1 is returned. + * + * If the provided reference points to a branch, the HEAD will point + * to that branch, staying attached, or become attached if it isn't yet. + * If the branch doesn't exist yet, no error will be return. The HEAD + * will then be attached to an unborn branch. + * + * Otherwise, the HEAD will be detached and will directly point to + * the Commit. + * + * @param repo Repository pointer + * @param refname Canonical name of the reference the HEAD should point at + * @return 0 on success, or an error code + */ +GIT_EXTERN(int) git_repository_set_head( + git_repository* repo, + const char* refname); + +/** + * Make the repository HEAD directly point to the Commit. + * + * If the provided committish cannot be found in the repository, the HEAD + * is unaltered and GIT_ENOTFOUND is returned. + * + * If the provided commitish cannot be peeled into a commit, the HEAD + * is unaltered and -1 is returned. + * + * Otherwise, the HEAD will eventually be detached and will directly point to + * the peeled Commit. + * + * @param repo Repository pointer + * @param commitish Object id of the Commit the HEAD should point to + * @return 0 on success, or an error code + */ +GIT_EXTERN(int) git_repository_set_head_detached( + git_repository* repo, + const git_oid* commitish); + +/** + * Detach the HEAD. + * + * If the HEAD is already detached and points to a Commit, 0 is returned. + * + * If the HEAD is already detached and points to a Tag, the HEAD is + * updated into making it point to the peeled Commit, and 0 is returned. + * + * If the HEAD is already detached and points to a non commitish, the HEAD is + * unaletered, and -1 is returned. + * + * Otherwise, the HEAD will be detached and point to the peeled Commit. + * + * @param repo Repository pointer + * @return 0 on success, or an error code + */ +GIT_EXTERN(int) git_repository_detach_head( + git_repository* repo); + /** @} */ GIT_END_DECL #endif diff --git a/include/git2/reset.h b/include/git2/reset.h index cd263fa99..cdcfb7671 100644 --- a/include/git2/reset.h +++ b/include/git2/reset.h @@ -24,6 +24,9 @@ GIT_BEGIN_DECL * Specifying a Mixed kind of reset will trigger a Soft reset and the index will * be replaced with the content of the commit tree. * + * Specifying a Hard kind of reset will trigger a Mixed reset and the working + * directory will be replaced with the content of the index. + * * TODO: Implement remaining kinds of resets. * * @param repo Repository where to perform the reset operation. diff --git a/include/git2/types.h b/include/git2/types.h index d3a905372..26e9c57e7 100644 --- a/include/git2/types.h +++ b/include/git2/types.h @@ -173,6 +173,7 @@ typedef enum { typedef enum { GIT_RESET_SOFT = 1, GIT_RESET_MIXED = 2, + GIT_RESET_HARD = 3, } git_reset_type; /** Valid modes for index and tree entries. */ diff --git a/src/checkout.c b/src/checkout.c index d1720fcf3..b20bd57e8 100644 --- a/src/checkout.c +++ b/src/checkout.c @@ -11,9 +11,9 @@ #include "git2/repository.h" #include "git2/refs.h" #include "git2/tree.h" -#include "git2/commit.h" #include "git2/blob.h" #include "git2/config.h" +#include "git2/diff.h" #include "common.h" #include "refs.h" @@ -22,204 +22,359 @@ #include "filter.h" #include "blob.h" -typedef struct tree_walk_data +struct checkout_diff_data { + git_buf *path; + int workdir_len; + git_checkout_opts *checkout_opts; git_indexer_stats *stats; - git_checkout_opts *opts; - git_repository *repo; - git_odb *odb; - bool no_symlinks; -} tree_walk_data; + git_repository *owner; + bool can_symlink; +}; - -static int blob_contents_to_link(tree_walk_data *data, git_buf *fnbuf, - const git_oid *id) +static int buffer_to_file( + git_buf *buffer, + const char *path, + int dir_mode, + int file_open_flags, + mode_t file_mode) { - int retcode = GIT_ERROR; - git_blob *blob; + int fd, error_write, error_close; - /* Get the link target */ - if (!(retcode = git_blob_lookup(&blob, data->repo, id))) { - git_buf linktarget = GIT_BUF_INIT; - if (!(retcode = git_blob__getbuf(&linktarget, blob))) { - /* Create the link */ - const char *new = git_buf_cstr(&linktarget), - *old = git_buf_cstr(fnbuf); - retcode = data->no_symlinks - ? git_futils_fake_symlink(new, old) - : p_symlink(new, old); - } - git_buf_free(&linktarget); - git_blob_free(blob); - } + if (git_futils_mkpath2file(path, dir_mode) < 0) + return -1; - return retcode; + if ((fd = p_open(path, file_open_flags, file_mode)) < 0) + return -1; + + error_write = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer)); + error_close = p_close(fd); + + return error_write ? error_write : error_close; } - -static int blob_contents_to_file(git_repository *repo, git_buf *fnbuf, - const git_tree_entry *entry, tree_walk_data *data) +static int blob_content_to_file( + git_blob *blob, + const char *path, + unsigned int entry_filemode, + git_checkout_opts *opts) { - int retcode = GIT_ERROR; - int fd = -1; - git_buf contents = GIT_BUF_INIT; - const git_oid *id = git_tree_entry_id(entry); - int file_mode = data->opts->file_mode; + int error, nb_filters = 0, file_mode = opts->file_mode; + bool dont_free_filtered = false; + git_buf unfiltered = GIT_BUF_INIT, filtered = GIT_BUF_INIT; + git_vector filters = GIT_VECTOR_INIT; - /* Deal with pre-existing files */ - if (git_path_exists(git_buf_cstr(fnbuf)) && - data->opts->existing_file_action == GIT_CHECKOUT_SKIP_EXISTING) - return 0; + if (opts->disable_filters || + (nb_filters = git_filters_load( + &filters, + git_object_owner((git_object *)blob), + path, + GIT_FILTER_TO_WORKTREE)) == 0) { - /* Allow disabling of filters */ - if (data->opts->disable_filters) { - git_blob *blob; - if (!(retcode = git_blob_lookup(&blob, repo, id))) { - retcode = git_blob__getbuf(&contents, blob); - git_blob_free(blob); - } - } else { - retcode = git_filter_blob_contents(&contents, repo, id, git_buf_cstr(fnbuf)); + /* Create a fake git_buf from the blob raw data... */ + filtered.ptr = blob->odb_object->raw.data; + filtered.size = blob->odb_object->raw.len; + + /* ... and make sure it doesn't get unexpectedly freed */ + dont_free_filtered = true; + } + + if (nb_filters < 0) + return nb_filters; + + if (nb_filters > 0) { + if (git_blob__getbuf(&unfiltered, blob) < 0) + goto cleanup; + + if ((error = git_filters_apply(&filtered, &unfiltered, &filters)) < 0) + goto cleanup; } - if (retcode < 0) goto bctf_cleanup; /* Allow overriding of file mode */ if (!file_mode) - file_mode = git_tree_entry_filemode(entry); + file_mode = entry_filemode; - if ((retcode = git_futils_mkpath2file(git_buf_cstr(fnbuf), data->opts->dir_mode)) < 0) - goto bctf_cleanup; + error = buffer_to_file(&filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); - fd = p_open(git_buf_cstr(fnbuf), data->opts->file_open_flags, file_mode); - if (fd < 0) goto bctf_cleanup; +cleanup: + git_filters_free(&filters); + git_buf_free(&unfiltered); + if (!dont_free_filtered) + git_buf_free(&filtered); - if (!p_write(fd, git_buf_cstr(&contents), git_buf_len(&contents))) - retcode = 0; - else - retcode = GIT_ERROR; - p_close(fd); - -bctf_cleanup: - git_buf_free(&contents); - return retcode; + return error; } -static int checkout_walker(const char *path, const git_tree_entry *entry, void *payload) +static int blob_content_to_link(git_blob *blob, const char *path, bool can_symlink) { - int retcode = 0; - tree_walk_data *data = (tree_walk_data*)payload; - int attr = git_tree_entry_filemode(entry); - git_buf fnbuf = GIT_BUF_INIT; - git_buf_join_n(&fnbuf, '/', 3, - git_repository_workdir(data->repo), - path, - git_tree_entry_name(entry)); + git_buf linktarget = GIT_BUF_INIT; + int error; - switch(git_tree_entry_type(entry)) - { - case GIT_OBJ_TREE: - /* Nothing to do; the blob handling creates necessary directories. */ + if (git_blob__getbuf(&linktarget, blob) < 0) + return -1; + + if (can_symlink) + error = p_symlink(git_buf_cstr(&linktarget), path); + else + error = git_futils_fake_symlink(git_buf_cstr(&linktarget), path); + + git_buf_free(&linktarget); + + return error; +} + +static int checkout_blob( + git_repository *repo, + git_oid *blob_oid, + const char *path, + unsigned int filemode, + bool can_symlink, + git_checkout_opts *opts) +{ + git_blob *blob; + int error; + + if (git_blob_lookup(&blob, repo, blob_oid) < 0) + return -1; /* Add an error message */ + + if (S_ISLNK(filemode)) + error = blob_content_to_link(blob, path, can_symlink); + else + error = blob_content_to_file(blob, path, filemode, opts); + + git_blob_free(blob); + + return error; +} + +static int checkout_diff_fn( + void *cb_data, + git_diff_delta *delta, + float progress) +{ + struct checkout_diff_data *data; + int error = -1; + + data = (struct checkout_diff_data *)cb_data; + + data->stats->processed = (unsigned int)(data->stats->total * progress); + + git_buf_truncate(data->path, data->workdir_len); + if (git_buf_joinpath(data->path, git_buf_cstr(data->path), delta->new_file.path) < 0) + return -1; + + switch (delta->status) { + case GIT_DELTA_UNTRACKED: + if (!(data->checkout_opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED)) + return 0; + + if (!git__suffixcmp(delta->new_file.path, "/")) + error = git_futils_rmdir_r(git_buf_cstr(data->path), GIT_DIRREMOVAL_FILES_AND_DIRS); + else + error = p_unlink(git_buf_cstr(data->path)); break; - case GIT_OBJ_COMMIT: - /* Submodule */ - git_futils_mkpath2file(git_buf_cstr(&fnbuf), data->opts->dir_mode); - retcode = p_mkdir(git_buf_cstr(&fnbuf), data->opts->dir_mode); + case GIT_DELTA_MODIFIED: + if (!(data->checkout_opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED)) + return 0; + + if (checkout_blob( + data->owner, + &delta->old_file.oid, + git_buf_cstr(data->path), + delta->old_file.mode, + data->can_symlink, + data->checkout_opts) < 0) + goto cleanup; + break; - case GIT_OBJ_BLOB: - if (S_ISLNK(attr)) { - retcode = blob_contents_to_link(data, &fnbuf, - git_tree_entry_id(entry)); - } else { - retcode = blob_contents_to_file(data->repo, &fnbuf, entry, data); - } + case GIT_DELTA_DELETED: + if (!(data->checkout_opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING)) + return 0; + + if (checkout_blob( + data->owner, + &delta->old_file.oid, + git_buf_cstr(data->path), + delta->old_file.mode, + data->can_symlink, + data->checkout_opts) < 0) + goto cleanup; + break; default: - retcode = -1; - break; + giterr_set(GITERR_INVALID, "Unexpected status (%d) for path '%s'.", delta->status, delta->new_file.path); + goto cleanup; } - git_buf_free(&fnbuf); - data->stats->processed++; - return retcode; + error = 0; + +cleanup: + return error; } - -int git_checkout_head(git_repository *repo, git_checkout_opts *opts, git_indexer_stats *stats) +static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink) { - int retcode = GIT_ERROR; - git_indexer_stats dummy_stats; - git_checkout_opts default_opts = {0}; - git_tree *tree; - tree_walk_data payload; git_config *cfg; + int error; - assert(repo); - if (!opts) opts = &default_opts; - if (!stats) stats = &dummy_stats; + if (git_repository_config__weakptr(&cfg, repo) < 0) + return -1; + + error = git_config_get_bool((int *)can_symlink, cfg, "core.symlinks"); + + /* + * When no "core.symlinks" entry is found in any of the configuration + * store (local, global or system), default value is "true". + */ + if (error == GIT_ENOTFOUND) { + *can_symlink = true; + error = 0; + } + + return error; +} + +static void normalize_options(git_checkout_opts *normalized, git_checkout_opts *proposed) +{ + assert(normalized); + + if (!proposed) + memset(normalized, 0, sizeof(git_checkout_opts)); + else + memmove(normalized, proposed, sizeof(git_checkout_opts)); /* Default options */ - if (!opts->existing_file_action) - opts->existing_file_action = GIT_CHECKOUT_OVERWRITE_EXISTING; - /* opts->disable_filters is false by default */ - if (!opts->dir_mode) opts->dir_mode = GIT_DIR_MODE; - if (!opts->file_open_flags) - opts->file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; + if (!normalized->checkout_strategy) + normalized->checkout_strategy = GIT_CHECKOUT_DEFAULT; - if (git_repository_is_bare(repo)) { - giterr_set(GITERR_INVALID, "Checkout is not allowed for bare repositories"); + /* opts->disable_filters is false by default */ + if (!normalized->dir_mode) + normalized->dir_mode = GIT_DIR_MODE; + + if (!normalized->file_open_flags) + normalized->file_open_flags = O_CREAT | O_TRUNC | O_WRONLY; +} + +int git_checkout_index( + git_repository *repo, + git_checkout_opts *opts, + git_indexer_stats *stats) +{ + git_index *index = NULL; + git_diff_list *diff = NULL; + git_indexer_stats dummy_stats; + + git_diff_options diff_opts = {0}; + git_checkout_opts checkout_opts; + + struct checkout_diff_data data; + git_buf workdir = GIT_BUF_INIT; + + int error; + + assert(repo); + + if ((git_repository__ensure_not_bare(repo, "checkout")) < 0) + return GIT_EBAREREPO; + + diff_opts.flags = GIT_DIFF_INCLUDE_UNTRACKED; + + if (opts && opts->paths.count > 0) + diff_opts.pathspec = opts->paths; + + if ((error = git_diff_workdir_to_index(repo, &diff_opts, &diff)) < 0) + goto cleanup; + + if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0) + goto cleanup; + + normalize_options(&checkout_opts, opts); + + if (!stats) + stats = &dummy_stats; + + stats->processed = 0; + + if ((git_repository_index(&index, repo)) < 0) + goto cleanup; + + stats->total = git_index_entrycount(index); + + memset(&data, 0, sizeof(data)); + + data.path = &workdir; + data.workdir_len = git_buf_len(&workdir); + data.checkout_opts = &checkout_opts; + data.stats = stats; + data.owner = repo; + + if ((error = retrieve_symlink_capabilities(repo, &data.can_symlink)) < 0) + goto cleanup; + + error = git_diff_foreach(diff, &data, checkout_diff_fn, NULL, NULL); + +cleanup: + git_index_free(index); + git_diff_list_free(diff); + git_buf_free(&workdir); + return error; +} + +int git_checkout_tree( + git_repository *repo, + git_object *treeish, + git_checkout_opts *opts, + git_indexer_stats *stats) +{ + git_index *index = NULL; + git_tree *tree = NULL; + + int error; + + assert(repo && treeish); + + if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) { + giterr_set(GITERR_INVALID, "Provided treeish cannot be peeled into a tree."); return GIT_ERROR; } - memset(&payload, 0, sizeof(payload)); + if ((error = git_repository_index(&index, repo)) < 0) + goto cleanup; - /* Determine if symlinks should be handled */ - if (!git_repository_config__weakptr(&cfg, repo)) { - int temp = true; - if (!git_config_get_bool(&temp, cfg, "core.symlinks")) { - payload.no_symlinks = !temp; - } - } + if ((error = git_index_read_tree(index, tree, NULL)) < 0) + goto cleanup; - stats->total = stats->processed = 0; - payload.stats = stats; - payload.opts = opts; - payload.repo = repo; - if (git_repository_odb(&payload.odb, repo) < 0) return GIT_ERROR; + if ((error = git_index_write(index)) < 0) + goto cleanup; - if (!git_repository_head_tree(&tree, repo)) { - git_index *idx; - if (!(retcode = git_repository_index(&idx, repo))) { - if (!(retcode = git_index_read_tree(idx, tree, stats))) { - git_index_write(idx); - retcode = git_tree_walk(tree, checkout_walker, GIT_TREEWALK_POST, &payload); - } - git_index_free(idx); - } - git_tree_free(tree); - } + error = git_checkout_index(repo, opts, stats); - git_odb_free(payload.odb); - return retcode; +cleanup: + git_index_free(index); + git_tree_free(tree); + return error; } - -int git_checkout_reference(git_reference *ref, - git_checkout_opts *opts, - git_indexer_stats *stats) +int git_checkout_head( + git_repository *repo, + git_checkout_opts *opts, + git_indexer_stats *stats) { - git_repository *repo= git_reference_owner(ref); - git_reference *head = NULL; - int retcode = GIT_ERROR; + int error; + git_tree *tree = NULL; - if ((retcode = git_reference_create_symbolic(&head, repo, GIT_HEAD_FILE, - git_reference_name(ref), true)) < 0) - return retcode; + assert(repo); - retcode = git_checkout_head(git_reference_owner(ref), opts, stats); + if (git_repository_head_tree(&tree, repo) < 0) + return -1; - git_reference_free(head); - return retcode; + error = git_checkout_tree(repo, (git_object *)tree, opts, stats); + + git_tree_free(tree); + + return error; } + diff --git a/src/clone.c b/src/clone.c index c4f6ec97e..80a13d0f2 100644 --- a/src/clone.c +++ b/src/clone.c @@ -235,9 +235,8 @@ int git_clone(git_repository **out, assert(out && origin_url && workdir_path); - if (!(retcode = clone_internal(out, origin_url, workdir_path, fetch_stats, 0))) { + if (!(retcode = clone_internal(out, origin_url, workdir_path, fetch_stats, 0))) retcode = git_checkout_head(*out, checkout_opts, checkout_stats); - } return retcode; } diff --git a/src/filter.c b/src/filter.c index e9517a259..28a05235b 100644 --- a/src/filter.c +++ b/src/filter.c @@ -164,38 +164,3 @@ int git_filters_apply(git_buf *dest, git_buf *source, git_vector *filters) return 0; } - -static int unfiltered_blob_contents(git_buf *out, git_repository *repo, const git_oid *blob_id) -{ - int retcode = GIT_ERROR; - git_blob *blob; - - if (!(retcode = git_blob_lookup(&blob, repo, blob_id))) - { - retcode = git_blob__getbuf(out, blob); - git_blob_free(blob); - } - - return retcode; -} - -int git_filter_blob_contents(git_buf *out, git_repository *repo, const git_oid *oid, const char *path) -{ - int retcode = GIT_ERROR; - - git_buf unfiltered = GIT_BUF_INIT; - if (!unfiltered_blob_contents(&unfiltered, repo, oid)) { - git_vector filters = GIT_VECTOR_INIT; - if (git_filters_load(&filters, - repo, path, GIT_FILTER_TO_WORKTREE) >= 0) { - git_buf_clear(out); - retcode = git_filters_apply(out, &unfiltered, &filters); - } - - git_filters_free(&filters); - } - - git_buf_free(&unfiltered); - return retcode; -} - diff --git a/src/filter.h b/src/filter.h index 5b7a25b04..b9beb4942 100644 --- a/src/filter.h +++ b/src/filter.h @@ -119,16 +119,4 @@ extern void git_text_gather_stats(git_text_stats *stats, const git_buf *text); */ extern int git_text_is_binary(git_text_stats *stats); - -/** - * Get the content of a blob after all filters have been run. - * - * @param out buffer to receive the contents - * @param repo repository containing the blob - * @param oid object id for the blob - * @param path path to the blob's output file, relative to the workdir root - * @return 0 on success, an error code otherwise - */ -extern int git_filter_blob_contents(git_buf *out, git_repository *repo, const git_oid *oid, const char *path); - #endif diff --git a/src/repository.c b/src/repository.c index 20a623a85..734cab43d 100644 --- a/src/repository.c +++ b/src/repository.c @@ -1442,3 +1442,85 @@ cleanup: return error; } +static bool looks_like_a_branch(const char *refname) +{ + return git__prefixcmp(refname, GIT_REFS_HEADS_DIR) == 0; +} + +int git_repository_set_head( + git_repository* repo, + const char* refname) +{ + git_reference *ref, + *new_head = NULL; + int error; + + assert(repo && refname); + + error = git_reference_lookup(&ref, repo, refname); + if (error < 0 && error != GIT_ENOTFOUND) + return error; + + if (!error) { + if (git_reference_is_branch(ref)) + error = git_reference_create_symbolic(&new_head, repo, GIT_HEAD_FILE, git_reference_name(ref), 1); + else + error = git_repository_set_head_detached(repo, git_reference_oid(ref)); + } else if (looks_like_a_branch(refname)) + error = git_reference_create_symbolic(&new_head, repo, GIT_HEAD_FILE, refname, 1); + + git_reference_free(ref); + git_reference_free(new_head); + return error; +} + +int git_repository_set_head_detached( + git_repository* repo, + const git_oid* commitish) +{ + int error; + git_object *object, + *peeled = NULL; + git_reference *new_head = NULL; + + assert(repo && commitish); + + if ((error = git_object_lookup(&object, repo, commitish, GIT_OBJ_ANY)) < 0) + return error; + + if ((error = git_object_peel(&peeled, object, GIT_OBJ_COMMIT)) < 0) + goto cleanup; + + error = git_reference_create_oid(&new_head, repo, GIT_HEAD_FILE, git_object_id(peeled), 1); + +cleanup: + git_object_free(object); + git_object_free(peeled); + git_reference_free(new_head); + return error; +} + +int git_repository_detach_head( + git_repository* repo) +{ + git_reference *old_head = NULL, + *new_head = NULL; + git_object *object = NULL; + int error = -1; + + assert(repo); + + if (git_repository_head(&old_head, repo) < 0) + return -1; + + if (git_object_lookup(&object, repo, git_reference_oid(old_head), GIT_OBJ_COMMIT) < 0) + goto cleanup; + + error = git_reference_create_oid(&new_head, repo, GIT_HEAD_FILE, git_reference_oid(old_head), 1); + +cleanup: + git_object_free(object); + git_reference_free(old_head); + git_reference_free(new_head); + return error; +} diff --git a/src/reset.c b/src/reset.c index 5aaf94840..4ce21e2cf 100644 --- a/src/reset.c +++ b/src/reset.c @@ -9,6 +9,7 @@ #include "commit.h" #include "tag.h" #include "git2/reset.h" +#include "git2/checkout.h" #define ERROR_MSG "Cannot perform reset" @@ -27,9 +28,12 @@ int git_reset( git_index *index = NULL; git_tree *tree = NULL; int error = -1; + git_checkout_opts opts; assert(repo && target); - assert(reset_type == GIT_RESET_SOFT || reset_type == GIT_RESET_MIXED); + assert(reset_type == GIT_RESET_SOFT + || reset_type == GIT_RESET_MIXED + || reset_type == GIT_RESET_HARD); if (git_object_owner(target) != repo) return reset_error_invalid("The given target does not belong to this repository."); @@ -73,6 +77,22 @@ int git_reset( goto cleanup; } + if (reset_type == GIT_RESET_MIXED) { + error = 0; + goto cleanup; + } + + memset(&opts, 0, sizeof(opts)); + opts.checkout_strategy = + GIT_CHECKOUT_CREATE_MISSING + | GIT_CHECKOUT_OVERWRITE_MODIFIED + | GIT_CHECKOUT_REMOVE_UNTRACKED; + + if (git_checkout_index(repo, &opts, NULL) < 0) { + giterr_set(GITERR_INDEX, "%s - Failed to checkout the index.", ERROR_MSG); + goto cleanup; + } + error = 0; cleanup: diff --git a/tests-clar/checkout/checkout.c b/tests-clar/checkout/checkout.c deleted file mode 100644 index ba14194c4..000000000 --- a/tests-clar/checkout/checkout.c +++ /dev/null @@ -1,206 +0,0 @@ -#include "clar_libgit2.h" - -#include "git2/checkout.h" -#include "repository.h" - - -static git_repository *g_repo; - -void test_checkout_checkout__initialize(void) -{ - const char *attributes = "* text eol=lf\n"; - - g_repo = cl_git_sandbox_init("testrepo"); - cl_git_mkfile("./testrepo/.gitattributes", attributes); -} - -void test_checkout_checkout__cleanup(void) -{ - cl_git_sandbox_cleanup(); -} - - -static void test_file_contents(const char *path, const char *expectedcontents) -{ - int fd; - char buffer[1024] = {0}; - size_t expectedlen, actuallen; - - fd = p_open(path, O_RDONLY); - cl_assert(fd >= 0); - - expectedlen = strlen(expectedcontents); - actuallen = p_read(fd, buffer, 1024); - cl_git_pass(p_close(fd)); - - cl_assert_equal_sz(actuallen, expectedlen); - cl_assert_equal_s(buffer, expectedcontents); -} - - -void test_checkout_checkout__bare(void) -{ - cl_git_sandbox_cleanup(); - g_repo = cl_git_sandbox_init("testrepo.git"); - cl_git_fail(git_checkout_head(g_repo, NULL, NULL)); -} - -void test_checkout_checkout__default(void) -{ - cl_git_pass(git_checkout_head(g_repo, NULL, NULL)); - test_file_contents("./testrepo/README", "hey there\n"); - test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); - test_file_contents("./testrepo/new.txt", "my new file\n"); -} - - -void test_checkout_checkout__crlf(void) -{ - const char *attributes = - "branch_file.txt text eol=crlf\n" - "new.txt text eol=lf\n"; - git_config *cfg; - - cl_git_pass(git_repository_config__weakptr(&cfg, g_repo)); - cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", false)); - cl_git_mkfile("./testrepo/.gitattributes", attributes); - - cl_git_pass(git_checkout_head(g_repo, NULL, NULL)); - test_file_contents("./testrepo/README", "hey there\n"); - test_file_contents("./testrepo/new.txt", "my new file\n"); - test_file_contents("./testrepo/branch_file.txt", "hi\r\nbye!\r\n"); -} - - -void test_checkout_checkout__win32_autocrlf(void) -{ -#ifdef GIT_WIN32 - git_config *cfg; - const char *expected_readme_text = "hey there\r\n"; - - cl_must_pass(p_unlink("./testrepo/.gitattributes")); - cl_git_pass(git_repository_config__weakptr(&cfg, g_repo)); - cl_git_pass(git_config_set_bool(cfg, "core.autocrlf", true)); - - cl_git_pass(git_checkout_head(g_repo, NULL, NULL)); - test_file_contents("./testrepo/README", expected_readme_text); -#endif -} - - -static void enable_symlinks(bool enable) -{ - git_config *cfg; - cl_git_pass(git_repository_config(&cfg, g_repo)); - cl_git_pass(git_config_set_bool(cfg, "core.symlinks", enable)); - git_config_free(cfg); -} - -void test_checkout_checkout__symlinks(void) -{ - /* First try with symlinks forced on */ - enable_symlinks(true); - cl_git_pass(git_checkout_head(g_repo, NULL, NULL)); - -#ifdef GIT_WIN32 - test_file_contents("./testrepo/link_to_new.txt", "new.txt"); -#else - { - char link_data[1024]; - size_t link_size = 1024; - - link_size = p_readlink("./testrepo/link_to_new.txt", link_data, link_size); - link_data[link_size] = '\0'; - cl_assert_equal_i(link_size, strlen("new.txt")); - cl_assert_equal_s(link_data, "new.txt"); - test_file_contents("./testrepo/link_to_new.txt", "my new file\n"); - } -#endif - - /* Now with symlinks forced off */ - cl_git_sandbox_cleanup(); - g_repo = cl_git_sandbox_init("testrepo"); - enable_symlinks(false); - cl_git_pass(git_checkout_head(g_repo, NULL, NULL)); - - test_file_contents("./testrepo/link_to_new.txt", "new.txt"); -} - -void test_checkout_checkout__existing_file_skip(void) -{ - git_checkout_opts opts = {0}; - cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); - opts.existing_file_action = GIT_CHECKOUT_SKIP_EXISTING; - cl_git_pass(git_checkout_head(g_repo, &opts, NULL)); - test_file_contents("./testrepo/new.txt", "This isn't what's stored!"); -} - -void test_checkout_checkout__existing_file_overwrite(void) -{ - git_checkout_opts opts = {0}; - cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); - opts.existing_file_action = GIT_CHECKOUT_OVERWRITE_EXISTING; - cl_git_pass(git_checkout_head(g_repo, &opts, NULL)); - test_file_contents("./testrepo/new.txt", "my new file\n"); -} - -void test_checkout_checkout__disable_filters(void) -{ - git_checkout_opts opts = {0}; - cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n"); - /* TODO cl_git_pass(git_checkout_head(g_repo, &opts, NULL));*/ - /* TODO test_file_contents("./testrepo/new.txt", "my new file\r\n");*/ - opts.disable_filters = true; - cl_git_pass(git_checkout_head(g_repo, &opts, NULL)); - test_file_contents("./testrepo/new.txt", "my new file\n"); -} - -void test_checkout_checkout__dir_modes(void) -{ -#ifndef GIT_WIN32 - git_checkout_opts opts = {0}; - struct stat st; - git_reference *ref; - - cl_git_pass(git_reference_lookup(&ref, g_repo, "refs/heads/dir")); - - opts.dir_mode = 0701; - cl_git_pass(git_checkout_reference(ref, &opts, NULL)); - cl_git_pass(p_stat("./testrepo/a", &st)); - cl_assert_equal_i(st.st_mode & 0777, 0701); - - /* File-mode test, since we're on the 'dir' branch */ - cl_git_pass(p_stat("./testrepo/a/b.txt", &st)); - cl_assert_equal_i(st.st_mode & 0777, 0755); - - git_reference_free(ref); -#endif -} - -void test_checkout_checkout__override_file_modes(void) -{ -#ifndef GIT_WIN32 - git_checkout_opts opts = {0}; - struct stat st; - - opts.file_mode = 0700; - cl_git_pass(git_checkout_head(g_repo, &opts, NULL)); - cl_git_pass(p_stat("./testrepo/new.txt", &st)); - cl_assert_equal_i(st.st_mode & 0777, 0700); -#endif -} - -void test_checkout_checkout__open_flags(void) -{ - git_checkout_opts opts = {0}; - - cl_git_mkfile("./testrepo/new.txt", "hi\n"); - opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND; - cl_git_pass(git_checkout_head(g_repo, &opts, NULL)); - test_file_contents("./testrepo/new.txt", "hi\nmy new file\n"); -} - -void test_checkout_checkout__detached_head(void) -{ - /* TODO: write this when git_checkout_commit is implemented. */ -} diff --git a/tests-clar/checkout/index.c b/tests-clar/checkout/index.c new file mode 100644 index 000000000..d1c59e38c --- /dev/null +++ b/tests-clar/checkout/index.c @@ -0,0 +1,289 @@ +#include "clar_libgit2.h" + +#include "git2/checkout.h" +#include "repository.h" + +static git_repository *g_repo; +static git_checkout_opts g_opts; + +static void reset_index_to_treeish(git_object *treeish) +{ + git_object *tree; + git_index *index; + + cl_git_pass(git_object_peel(&tree, treeish, GIT_OBJ_TREE)); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_read_tree(index, (git_tree *)tree, NULL)); + cl_git_pass(git_index_write(index)); + + git_object_free(tree); + git_index_free(index); +} + +void test_checkout_index__initialize(void) +{ + git_tree *tree; + + memset(&g_opts, 0, sizeof(g_opts)); + g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + + g_repo = cl_git_sandbox_init("testrepo"); + + cl_git_pass(git_repository_head_tree(&tree, g_repo)); + + reset_index_to_treeish((git_object *)tree); + git_tree_free(tree); + + cl_git_rewritefile( + "./testrepo/.gitattributes", + "* text eol=lf\n"); +} + +void test_checkout_index__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static void test_file_contents(const char *path, const char *expectedcontents) +{ + int fd; + char buffer[1024] = {0}; + size_t expectedlen, actuallen; + + fd = p_open(path, O_RDONLY); + cl_assert(fd >= 0); + + expectedlen = strlen(expectedcontents); + actuallen = p_read(fd, buffer, 1024); + cl_git_pass(p_close(fd)); + + cl_assert_equal_sz(actuallen, expectedlen); + cl_assert_equal_s(buffer, expectedcontents); +} + +void test_checkout_index__cannot_checkout_a_bare_repository(void) +{ + test_checkout_index__cleanup(); + + memset(&g_opts, 0, sizeof(g_opts)); + g_repo = cl_git_sandbox_init("testrepo.git"); + + cl_git_fail(git_checkout_index(g_repo, NULL, NULL)); +} + +void test_checkout_index__can_create_missing_files(void) +{ + cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); + cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt")); + cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); + + g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/README", "hey there\n"); + test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); + test_file_contents("./testrepo/new.txt", "my new file\n"); +} + +void test_checkout_index__can_remove_untracked_files(void) +{ + git_futils_mkdir("./testrepo/dir/subdir/subsubdir", NULL, 0755, GIT_MKDIR_PATH); + cl_git_mkfile("./testrepo/dir/one", "one\n"); + cl_git_mkfile("./testrepo/dir/subdir/two", "two\n"); + + cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir")); + + g_opts.checkout_strategy = GIT_CHECKOUT_REMOVE_UNTRACKED; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + cl_assert_equal_i(false, git_path_isdir("./testrepo/dir")); +} + +void test_checkout_index__honor_the_specified_pathspecs(void) +{ + char *entries[] = { "*.txt" }; + + g_opts.paths.strings = entries; + g_opts.paths.count = 1; + + cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); + cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt")); + cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt")); + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + cl_assert_equal_i(false, git_path_isfile("./testrepo/README")); + test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n"); + test_file_contents("./testrepo/new.txt", "my new file\n"); +} + +static void set_config_entry_to(const char *entry_name, bool value) +{ + git_config *cfg; + + cl_git_pass(git_repository_config(&cfg, g_repo)); + cl_git_pass(git_config_set_bool(cfg, entry_name, value)); + + git_config_free(cfg); +} + +static void set_core_autocrlf_to(bool value) +{ + set_config_entry_to("core.autocrlf", value); +} + +void test_checkout_index__honor_the_gitattributes_directives(void) +{ + const char *attributes = + "branch_file.txt text eol=crlf\n" + "new.txt text eol=lf\n"; + + cl_git_mkfile("./testrepo/.gitattributes", attributes); + set_core_autocrlf_to(false); + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/README", "hey there\n"); + test_file_contents("./testrepo/new.txt", "my new file\n"); + test_file_contents("./testrepo/branch_file.txt", "hi\r\nbye!\r\n"); +} + +void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void) +{ +#ifdef GIT_WIN32 + const char *expected_readme_text = "hey there\r\n"; + + cl_git_pass(p_unlink("./testrepo/.gitattributes")); + set_core_autocrlf_to(true); + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/README", expected_readme_text); +#endif +} + +static void set_repo_symlink_handling_cap_to(bool value) +{ + set_config_entry_to("core.symlinks", value); +} + +void test_checkout_index__honor_coresymlinks_setting_set_to_true(void) +{ + set_repo_symlink_handling_cap_to(true); + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + +#ifdef GIT_WIN32 + test_file_contents("./testrepo/link_to_new.txt", "new.txt"); +#else + { + char link_data[1024]; + size_t link_size = 1024; + + link_size = p_readlink("./testrepo/link_to_new.txt", link_data, link_size); + link_data[link_size] = '\0'; + cl_assert_equal_i(link_size, strlen("new.txt")); + cl_assert_equal_s(link_data, "new.txt"); + test_file_contents("./testrepo/link_to_new.txt", "my new file\n"); + } +#endif +} + +void test_checkout_index__honor_coresymlinks_setting_set_to_false(void) +{ + set_repo_symlink_handling_cap_to(false); + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/link_to_new.txt", "new.txt"); +} + +void test_checkout_index__donot_overwrite_modified_file_by_default(void) +{ + cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); + + g_opts.checkout_strategy = 0; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/new.txt", "This isn't what's stored!"); +} + +void test_checkout_index__can_overwrite_modified_file(void) +{ + cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!"); + + g_opts.checkout_strategy = GIT_CHECKOUT_OVERWRITE_MODIFIED; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/new.txt", "my new file\n"); +} + +void test_checkout_index__options_disable_filters(void) +{ + cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n"); + + g_opts.disable_filters = false; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/new.txt", "my new file\r\n"); + + p_unlink("./testrepo/new.txt"); + + g_opts.disable_filters = true; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/new.txt", "my new file\n"); +} + +void test_checkout_index__options_dir_modes(void) +{ +#ifndef GIT_WIN32 + struct stat st; + git_oid oid; + git_commit *commit; + + cl_git_pass(git_reference_name_to_oid(&oid, g_repo, "refs/heads/dir")); + cl_git_pass(git_commit_lookup(&commit, g_repo, &oid)); + + reset_index_to_treeish((git_object *)commit); + + g_opts.dir_mode = 0701; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + cl_git_pass(p_stat("./testrepo/a", &st)); + cl_assert_equal_i(st.st_mode & 0777, 0701); + + /* File-mode test, since we're on the 'dir' branch */ + cl_git_pass(p_stat("./testrepo/a/b.txt", &st)); + cl_assert_equal_i(st.st_mode & 0777, 0755); + + git_commit_free(commit); +#endif +} + +void test_checkout_index__options_override_file_modes(void) +{ +#ifndef GIT_WIN32 + struct stat st; + + g_opts.file_mode = 0700; + + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + cl_git_pass(p_stat("./testrepo/new.txt", &st)); + cl_assert_equal_i(st.st_mode & 0777, 0700); +#endif +} + +void test_checkout_index__options_open_flags(void) +{ + cl_git_mkfile("./testrepo/new.txt", "hi\n"); + + g_opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND; + + g_opts.checkout_strategy |= GIT_CHECKOUT_OVERWRITE_MODIFIED; + cl_git_pass(git_checkout_index(g_repo, &g_opts, NULL)); + + test_file_contents("./testrepo/new.txt", "hi\nmy new file\n"); +} diff --git a/tests-clar/checkout/tree.c b/tests-clar/checkout/tree.c new file mode 100644 index 000000000..6d573bfd7 --- /dev/null +++ b/tests-clar/checkout/tree.c @@ -0,0 +1,65 @@ +#include "clar_libgit2.h" + +#include "git2/checkout.h" +#include "repository.h" + +static git_repository *g_repo; +static git_checkout_opts g_opts; +static git_object *g_object; + +void test_checkout_tree__initialize(void) +{ + g_repo = cl_git_sandbox_init("testrepo"); + + memset(&g_opts, 0, sizeof(g_opts)); + g_opts.checkout_strategy = GIT_CHECKOUT_CREATE_MISSING; +} + +void test_checkout_tree__cleanup(void) +{ + git_object_free(g_object); + + cl_git_sandbox_cleanup(); +} + +void test_checkout_tree__cannot_checkout_a_non_treeish(void) +{ + /* blob */ + cl_git_pass(git_revparse_single(&g_object, g_repo, "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd")); + + cl_git_fail(git_checkout_tree(g_repo, g_object, NULL, NULL)); +} + +void test_checkout_tree__can_checkout_a_subdirectory_from_a_commit(void) +{ + char *entries[] = { "ab/de/" }; + + g_opts.paths.strings = entries; + g_opts.paths.count = 1; + + cl_git_pass(git_revparse_single(&g_object, g_repo, "subtrees")); + + cl_assert_equal_i(false, git_path_isdir("./testrepo/ab/")); + + cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts, NULL)); + + cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/2.txt")); + cl_assert_equal_i(true, git_path_isfile("./testrepo/ab/de/fgh/1.txt")); +} + +void test_checkout_tree__can_checkout_a_subdirectory_from_a_subtree(void) +{ + char *entries[] = { "de/" }; + + g_opts.paths.strings = entries; + g_opts.paths.count = 1; + + cl_git_pass(git_revparse_single(&g_object, g_repo, "subtrees:ab")); + + cl_assert_equal_i(false, git_path_isdir("./testrepo/de/")); + + cl_git_pass(git_checkout_tree(g_repo, g_object, &g_opts, NULL)); + + cl_assert_equal_i(true, git_path_isfile("./testrepo/de/2.txt")); + cl_assert_equal_i(true, git_path_isfile("./testrepo/de/fgh/1.txt")); +} diff --git a/tests-clar/repo/getters.c b/tests-clar/repo/getters.c index 966de1f16..ffcd171f2 100644 --- a/tests-clar/repo/getters.c +++ b/tests-clar/repo/getters.c @@ -23,52 +23,6 @@ void test_repo_getters__empty(void) git_repository_free(repo_empty); } -void test_repo_getters__head_detached(void) -{ - git_repository *repo; - git_reference *ref; - git_oid oid; - - cl_git_pass(git_repository_open(&repo, "testrepo.git")); - - cl_assert(git_repository_head_detached(repo) == 0); - - /* detach the HEAD */ - git_oid_fromstr(&oid, "c47800c7266a2be04c571c04d5a6614691ea99bd"); - cl_git_pass(git_reference_create_oid(&ref, repo, "HEAD", &oid, 1)); - cl_assert(git_repository_head_detached(repo) == 1); - git_reference_free(ref); - - /* take the reop back to it's original state */ - cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/master", 1)); - cl_assert(git_repository_head_detached(repo) == 0); - - git_reference_free(ref); - git_repository_free(repo); -} - -void test_repo_getters__head_orphan(void) -{ - git_repository *repo; - git_reference *ref; - - cl_git_pass(git_repository_open(&repo, "testrepo.git")); - - cl_assert(git_repository_head_orphan(repo) == 0); - - /* orphan HEAD */ - cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/orphan", 1)); - cl_assert(git_repository_head_orphan(repo) == 1); - git_reference_free(ref); - - /* take the reop back to it's original state */ - cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/master", 1)); - cl_assert(git_repository_head_orphan(repo) == 0); - - git_reference_free(ref); - git_repository_free(repo); -} - void test_repo_getters__retrieving_the_odb_honors_the_refcount(void) { git_odb *odb; diff --git a/tests-clar/repo/head.c b/tests-clar/repo/head.c new file mode 100644 index 000000000..64dec69dd --- /dev/null +++ b/tests-clar/repo/head.c @@ -0,0 +1,165 @@ +#include "clar_libgit2.h" +#include "refs.h" + +git_repository *repo; + +void test_repo_head__initialize(void) +{ + repo = cl_git_sandbox_init("testrepo.git"); +} + +void test_repo_head__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_repo_head__head_detached(void) +{ + git_reference *ref; + git_oid oid; + + cl_assert(git_repository_head_detached(repo) == 0); + + /* detach the HEAD */ + git_oid_fromstr(&oid, "c47800c7266a2be04c571c04d5a6614691ea99bd"); + cl_git_pass(git_reference_create_oid(&ref, repo, "HEAD", &oid, 1)); + cl_assert(git_repository_head_detached(repo) == 1); + git_reference_free(ref); + + /* take the reop back to it's original state */ + cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/master", 1)); + cl_assert(git_repository_head_detached(repo) == 0); + + git_reference_free(ref); +} + +void test_repo_head__head_orphan(void) +{ + git_reference *ref; + + cl_assert(git_repository_head_orphan(repo) == 0); + + /* orphan HEAD */ + cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/orphan", 1)); + cl_assert(git_repository_head_orphan(repo) == 1); + git_reference_free(ref); + + /* take the reop back to it's original state */ + cl_git_pass(git_reference_create_symbolic(&ref, repo, "HEAD", "refs/heads/master", 1)); + cl_assert(git_repository_head_orphan(repo) == 0); + + git_reference_free(ref); +} + +void test_repo_head__set_head_Attaches_HEAD_to_un_unborn_branch_when_the_branch_doesnt_exist(void) +{ + git_reference *head; + + cl_git_pass(git_repository_set_head(repo, "refs/heads/doesnt/exist/yet")); + + cl_assert_equal_i(false, git_repository_head_detached(repo)); + + cl_assert_equal_i(GIT_ENOTFOUND, git_repository_head(&head, repo)); +} + +void test_repo_head__set_head_Returns_ENOTFOUND_when_the_reference_doesnt_exist(void) +{ + cl_assert_equal_i(GIT_ENOTFOUND, git_repository_set_head(repo, "refs/tags/doesnt/exist/yet")); +} + +void test_repo_head__set_head_Fails_when_the_reference_points_to_a_non_commitish(void) +{ + cl_git_fail(git_repository_set_head(repo, "refs/tags/point_to_blob")); +} + +void test_repo_head__set_head_Attaches_HEAD_when_the_reference_points_to_a_branch(void) +{ + git_reference *head; + + cl_git_pass(git_repository_set_head(repo, "refs/heads/br2")); + + cl_assert_equal_i(false, git_repository_head_detached(repo)); + + cl_git_pass(git_repository_head(&head, repo)); + cl_assert_equal_s("refs/heads/br2", git_reference_name(head)); + + git_reference_free(head); +} + +static void assert_head_is_correctly_detached(void) +{ + git_reference *head; + git_object *commit; + + cl_assert_equal_i(true, git_repository_head_detached(repo)); + + cl_git_pass(git_repository_head(&head, repo)); + + cl_git_pass(git_object_lookup(&commit, repo, git_reference_oid(head), GIT_OBJ_COMMIT)); + + git_object_free(commit); + git_reference_free(head); +} + +void test_repo_head__set_head_Detaches_HEAD_when_the_reference_doesnt_point_to_a_branch(void) +{ + cl_git_pass(git_repository_set_head(repo, "refs/tags/test")); + + cl_assert_equal_i(true, git_repository_head_detached(repo)); + + assert_head_is_correctly_detached(); +} + +void test_repo_head__set_head_detached_Return_ENOTFOUND_when_the_object_doesnt_exist(void) +{ + git_oid oid; + + cl_git_pass(git_oid_fromstr(&oid, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")); + + cl_assert_equal_i(GIT_ENOTFOUND, git_repository_set_head_detached(repo, &oid)); +} + +void test_repo_head__set_head_detached_Fails_when_the_object_isnt_a_commitish(void) +{ + git_object *blob; + + cl_git_pass(git_revparse_single(&blob, repo, "point_to_blob")); + + cl_git_fail(git_repository_set_head_detached(repo, git_object_id(blob))); + + git_object_free(blob); +} + +void test_repo_head__set_head_detached_Detaches_HEAD_and_make_it_point_to_the_peeled_commit(void) +{ + git_object *tag; + + cl_git_pass(git_revparse_single(&tag, repo, "tags/test")); + cl_assert_equal_i(GIT_OBJ_TAG, git_object_type(tag)); + + cl_git_pass(git_repository_set_head_detached(repo, git_object_id(tag))); + + assert_head_is_correctly_detached(); + + git_object_free(tag); +} + +void test_repo_head__detach_head_Detaches_HEAD_and_make_it_point_to_the_peeled_commit(void) +{ + cl_assert_equal_i(false, git_repository_head_detached(repo)); + + cl_git_pass(git_repository_detach_head(repo)); + + assert_head_is_correctly_detached(); +} + +void test_repo_head__detach_head_Fails_if_HEAD_and_point_to_a_non_commitish(void) +{ + git_reference *head; + + cl_git_pass(git_reference_create_symbolic(&head, repo, GIT_HEAD_FILE, "refs/tags/point_to_blob", 1)); + + cl_git_fail(git_repository_detach_head(repo)); + + git_reference_free(head); +} diff --git a/tests-clar/reset/hard.c b/tests-clar/reset/hard.c new file mode 100644 index 000000000..ad3badb8a --- /dev/null +++ b/tests-clar/reset/hard.c @@ -0,0 +1,46 @@ +#include "clar_libgit2.h" +#include "posix.h" +#include "reset_helpers.h" +#include "path.h" +#include "fileops.h" + +static git_repository *repo; +static git_object *target; + +void test_reset_hard__initialize(void) +{ + repo = cl_git_sandbox_init("status"); + target = NULL; +} + +void test_reset_hard__cleanup(void) +{ + git_object_free(target); + cl_git_sandbox_cleanup(); +} + +void test_reset_hard__resetting_culls_empty_directories(void) +{ + git_buf subdir_path = GIT_BUF_INIT; + git_buf subfile_path = GIT_BUF_INIT; + git_buf newdir_path = GIT_BUF_INIT; + + cl_git_pass(git_buf_joinpath(&newdir_path, git_repository_workdir(repo), "newdir/")); + + cl_git_pass(git_buf_joinpath(&subfile_path, git_buf_cstr(&newdir_path), "with/nested/file.txt")); + cl_git_pass(git_futils_mkpath2file(git_buf_cstr(&subfile_path), 0755)); + cl_git_mkfile(git_buf_cstr(&subfile_path), "all anew...\n"); + + cl_git_pass(git_buf_joinpath(&subdir_path, git_repository_workdir(repo), "subdir/")); + cl_assert(git_path_isdir(git_buf_cstr(&subdir_path)) == true); + + retrieve_target_from_oid(&target, repo, "0017bd4ab1ec30440b17bae1680cff124ab5f1f6"); + cl_git_pass(git_reset(repo, target, GIT_RESET_HARD)); + + cl_assert(git_path_isdir(git_buf_cstr(&subdir_path)) == false); + cl_assert(git_path_isdir(git_buf_cstr(&newdir_path)) == false); + + git_buf_free(&subdir_path); + git_buf_free(&subfile_path); + git_buf_free(&newdir_path); +}