blob: e068e4f5f3e00564fe0c801367457608a5a7ac93 [file] [log] [blame]
/*
* 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;
}