| /* |
| * Copyright (C) 2009-2012 the libgit2 contributors |
| * |
| * 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 <assert.h> |
| |
| #include "git2/checkout.h" |
| #include "git2/repository.h" |
| #include "git2/refs.h" |
| #include "git2/tree.h" |
| #include "git2/blob.h" |
| #include "git2/config.h" |
| #include "git2/diff.h" |
| |
| #include "common.h" |
| #include "refs.h" |
| #include "buffer.h" |
| #include "repository.h" |
| #include "filter.h" |
| #include "blob.h" |
| |
| struct checkout_diff_data |
| { |
| git_buf *path; |
| size_t workdir_len; |
| git_checkout_opts *checkout_opts; |
| git_repository *owner; |
| bool can_symlink; |
| bool found_submodules; |
| bool create_submodules; |
| int error; |
| size_t total_steps; |
| size_t completed_steps; |
| }; |
| |
| static int buffer_to_file( |
| git_buf *buffer, |
| const char *path, |
| mode_t dir_mode, |
| int file_open_flags, |
| mode_t file_mode) |
| { |
| int fd, error, error_close; |
| |
| if ((error = git_futils_mkpath2file(path, dir_mode)) < 0) |
| return error; |
| |
| if ((fd = p_open(path, file_open_flags, file_mode)) < 0) |
| return fd; |
| |
| error = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer)); |
| |
| error_close = p_close(fd); |
| |
| if (!error) |
| error = error_close; |
| |
| if (!error && |
| (file_mode & 0100) != 0 && |
| (error = p_chmod(path, file_mode)) < 0) |
| giterr_set(GITERR_OS, "Failed to set permissions on '%s'", path); |
| |
| return error; |
| } |
| |
| static int blob_content_to_file( |
| git_blob *blob, |
| const char *path, |
| mode_t entry_filemode, |
| git_checkout_opts *opts) |
| { |
| int error = -1, nb_filters = 0; |
| mode_t 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; |
| |
| if (opts->disable_filters || |
| (nb_filters = git_filters_load( |
| &filters, |
| git_object_owner((git_object *)blob), |
| path, |
| GIT_FILTER_TO_WORKTREE)) == 0) { |
| |
| /* 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 ((error = git_blob__getbuf(&unfiltered, blob)) < 0) |
| goto cleanup; |
| |
| if ((error = git_filters_apply(&filtered, &unfiltered, &filters)) < 0) |
| goto cleanup; |
| } |
| |
| /* Allow overriding of file mode */ |
| if (!file_mode) |
| file_mode = entry_filemode; |
| |
| error = buffer_to_file(&filtered, path, opts->dir_mode, opts->file_open_flags, file_mode); |
| |
| cleanup: |
| git_filters_free(&filters); |
| git_buf_free(&unfiltered); |
| if (!dont_free_filtered) |
| git_buf_free(&filtered); |
| |
| return error; |
| } |
| |
| static int blob_content_to_link(git_blob *blob, const char *path, bool can_symlink) |
| { |
| git_buf linktarget = GIT_BUF_INIT; |
| int error; |
| |
| if ((error = git_blob__getbuf(&linktarget, blob)) < 0) |
| return error; |
| |
| 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_submodule( |
| struct checkout_diff_data *data, |
| const git_diff_file *file) |
| { |
| if (git_futils_mkdir( |
| file->path, git_repository_workdir(data->owner), |
| data->checkout_opts->dir_mode, GIT_MKDIR_PATH) < 0) |
| return -1; |
| |
| /* TODO: two cases: |
| * 1 - submodule already checked out, but we need to move the HEAD |
| * to the new OID, or |
| * 2 - submodule not checked out and we should recursively check it out |
| * |
| * Checkout will not execute a pull request on the submodule, but a |
| * clone command should probably be able to. Do we need a submodule |
| * callback option? |
| */ |
| |
| return 0; |
| } |
| |
| static void report_progress( |
| struct checkout_diff_data *data, |
| const char *path) |
| { |
| if (data->checkout_opts->progress_cb) |
| data->checkout_opts->progress_cb( |
| path, |
| data->completed_steps, |
| data->total_steps, |
| data->checkout_opts->progress_payload); |
| } |
| |
| static int checkout_blob( |
| struct checkout_diff_data *data, |
| const git_diff_file *file) |
| { |
| git_blob *blob; |
| int error; |
| |
| git_buf_truncate(data->path, data->workdir_len); |
| if (git_buf_joinpath(data->path, git_buf_cstr(data->path), file->path) < 0) |
| return -1; |
| |
| if ((error = git_blob_lookup(&blob, data->owner, &file->oid)) < 0) |
| return error; |
| |
| if (S_ISLNK(file->mode)) |
| error = blob_content_to_link( |
| blob, git_buf_cstr(data->path), data->can_symlink); |
| else |
| error = blob_content_to_file( |
| blob, git_buf_cstr(data->path), file->mode, data->checkout_opts); |
| |
| git_blob_free(blob); |
| |
| return error; |
| } |
| |
| static int checkout_remove_the_old( |
| void *cb_data, const git_diff_delta *delta, float progress) |
| { |
| struct checkout_diff_data *data = cb_data; |
| git_checkout_opts *opts = data->checkout_opts; |
| |
| GIT_UNUSED(progress); |
| |
| if ((delta->status == GIT_DELTA_UNTRACKED && |
| (opts->checkout_strategy & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0) || |
| (delta->status == GIT_DELTA_TYPECHANGE && |
| (opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0)) |
| { |
| data->error = git_futils_rmdir_r( |
| delta->new_file.path, |
| git_repository_workdir(data->owner), |
| GIT_DIRREMOVAL_FILES_AND_DIRS); |
| |
| data->completed_steps++; |
| report_progress(data, delta->new_file.path); |
| } |
| |
| return data->error; |
| } |
| |
| static int checkout_create_the_new( |
| void *cb_data, const git_diff_delta *delta, float progress) |
| { |
| int error = 0; |
| struct checkout_diff_data *data = cb_data; |
| git_checkout_opts *opts = data->checkout_opts; |
| bool do_checkout = false, do_notify = false; |
| |
| GIT_UNUSED(progress); |
| |
| if (delta->status == GIT_DELTA_MODIFIED || |
| delta->status == GIT_DELTA_TYPECHANGE) |
| { |
| if ((opts->checkout_strategy & GIT_CHECKOUT_OVERWRITE_MODIFIED) != 0) |
| do_checkout = true; |
| else if (opts->skipped_notify_cb != NULL) |
| do_notify = !data->create_submodules; |
| } |
| else if (delta->status == GIT_DELTA_DELETED && |
| (opts->checkout_strategy & GIT_CHECKOUT_CREATE_MISSING) != 0) |
| do_checkout = true; |
| |
| if (do_notify) { |
| if (opts->skipped_notify_cb( |
| delta->old_file.path, &delta->old_file.oid, |
| delta->old_file.mode, opts->notify_payload)) |
| { |
| giterr_clear(); |
| error = GIT_EUSER; |
| } |
| } |
| |
| if (do_checkout) { |
| bool is_submodule = S_ISGITLINK(delta->old_file.mode); |
| |
| if (is_submodule) { |
| data->found_submodules = true; |
| } |
| |
| if (!is_submodule && !data->create_submodules) { |
| error = checkout_blob(data, &delta->old_file); |
| data->completed_steps++; |
| report_progress(data, delta->old_file.path); |
| } |
| |
| else if (is_submodule && data->create_submodules) { |
| error = checkout_submodule(data, &delta->old_file); |
| data->completed_steps++; |
| report_progress(data, delta->old_file.path); |
| } |
| |
| } |
| |
| if (error) |
| data->error = error; |
| |
| return error; |
| } |
| |
| static int retrieve_symlink_capabilities(git_repository *repo, bool *can_symlink) |
| { |
| git_config *cfg; |
| int error; |
| |
| 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 (!normalized->checkout_strategy) |
| normalized->checkout_strategy = GIT_CHECKOUT_DEFAULT; |
| |
| /* 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_diff_list *diff = NULL; |
| |
| 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 ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0) |
| return error; |
| |
| diff_opts.flags = |
| GIT_DIFF_INCLUDE_UNTRACKED | |
| GIT_DIFF_INCLUDE_TYPECHANGE | |
| GIT_DIFF_SKIP_BINARY_CHECK; |
| |
| 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); |
| |
| memset(&data, 0, sizeof(data)); |
| |
| data.path = &workdir; |
| data.workdir_len = git_buf_len(&workdir); |
| data.checkout_opts = &checkout_opts; |
| data.owner = repo; |
| data.total_steps = (size_t)git_diff_num_deltas(diff); |
| |
| if ((error = retrieve_symlink_capabilities(repo, &data.can_symlink)) < 0) |
| goto cleanup; |
| |
| /* Checkout is best performed with three passes through the diff. |
| * |
| * 1. First do removes, because we iterate in alphabetical order, thus |
| * a new untracked directory will end up sorted *after* a blob that |
| * should be checked out with the same name. |
| * 2. Then checkout all blobs. |
| * 3. Then checkout all submodules in case a new .gitmodules blob was |
| * checked out during pass #2. |
| */ |
| |
| report_progress(&data, NULL); |
| |
| if (!(error = git_diff_foreach( |
| diff, &data, checkout_remove_the_old, NULL, NULL)) && |
| !(error = git_diff_foreach( |
| diff, &data, checkout_create_the_new, NULL, NULL)) && |
| data.found_submodules) |
| { |
| data.create_submodules = true; |
| error = git_diff_foreach( |
| diff, &data, checkout_create_the_new, NULL, NULL); |
| } |
| |
| cleanup: |
| if (error == GIT_EUSER) |
| error = (data.error != 0) ? data.error : -1; |
| |
| 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_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; |
| } |
| |
| if ((error = git_repository_index(&index, repo)) < 0) |
| goto cleanup; |
| |
| if ((error = git_index_read_tree(index, tree)) < 0) |
| goto cleanup; |
| |
| if ((error = git_index_write(index)) < 0) |
| goto cleanup; |
| |
| error = git_checkout_index(repo, opts); |
| |
| cleanup: |
| git_index_free(index); |
| git_tree_free(tree); |
| return error; |
| } |
| |
| int git_checkout_head( |
| git_repository *repo, |
| git_checkout_opts *opts) |
| { |
| git_reference *head; |
| int error; |
| git_object *tree = NULL; |
| |
| assert(repo); |
| |
| if ((error = git_repository_head(&head, repo)) < 0) |
| return error; |
| |
| if ((error = git_reference_peel(&tree, head, GIT_OBJ_TREE)) < 0) |
| goto cleanup; |
| |
| error = git_checkout_tree(repo, tree, opts); |
| |
| cleanup: |
| git_reference_free(head); |
| git_object_free(tree); |
| |
| return error; |
| } |