blob: 5229357fbb6cfb3984bddce658c52074c26dff7d [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"
#include "diff.h"
#include "pathspec.h"
typedef struct {
git_repository *repo;
git_diff_list *diff;
git_checkout_opts *opts;
git_buf *path;
size_t workdir_len;
bool can_symlink;
int error;
size_t total_steps;
size_t completed_steps;
} checkout_diff_data;
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;
if ((error = git_futils_mkpath2file(path, dir_mode)) < 0)
return error;
if ((fd = p_open(path, file_open_flags, file_mode)) < 0) {
giterr_set(GITERR_OS, "Could not open '%s' for writing", path);
return fd;
}
if ((error = p_write(fd, git_buf_cstr(buffer), git_buf_len(buffer))) < 0) {
giterr_set(GITERR_OS, "Could not write to '%s'", path);
(void)p_close(fd);
} else {
if ((error = p_close(fd)) < 0)
giterr_set(GITERR_OS, "Error while closing '%s'", path);
}
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(
checkout_diff_data *data,
const git_diff_file *file)
{
/* Until submodules are supported, UPDATE_ONLY means do nothing here */
if ((data->opts->checkout_strategy & GIT_CHECKOUT_UPDATE_ONLY) != 0)
return 0;
if (git_futils_mkdir(
file->path, git_repository_workdir(data->repo),
data->opts->dir_mode, GIT_MKDIR_PATH) < 0)
return -1;
/* TODO: Support checkout_strategy options. Two circumstances:
* 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 on the submodule, but a clone
* command should probably be able to. Do we need a submodule callback?
*/
return 0;
}
static void report_progress(
checkout_diff_data *data,
const char *path)
{
if (data->opts->progress_cb)
data->opts->progress_cb(
path, data->completed_steps, data->total_steps,
data->opts->progress_payload);
}
static int checkout_blob(
checkout_diff_data *data,
const git_diff_file *file)
{
int error = 0;
git_blob *blob;
git_buf_truncate(data->path, data->workdir_len);
if (git_buf_puts(data->path, file->path) < 0)
return -1;
if ((error = git_blob_lookup(&blob, data->repo, &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->opts);
git_blob_free(blob);
return error;
}
static int retrieve_symlink_caps(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");
/* If "core.symlinks" is not found anywhere, default to 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));
/* implied checkout strategies */
if ((normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_MODIFIED) != 0 ||
(normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0)
normalized->checkout_strategy |= GIT_CHECKOUT_UPDATE_UNMODIFIED;
if ((normalized->checkout_strategy & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0)
normalized->checkout_strategy |= GIT_CHECKOUT_UPDATE_MISSING;
/* 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;
}
enum {
CHECKOUT_ACTION__NONE = 0,
CHECKOUT_ACTION__REMOVE = 1,
CHECKOUT_ACTION__UPDATE_BLOB = 2,
CHECKOUT_ACTION__UPDATE_SUBMODULE = 4,
CHECKOUT_ACTION__CONFLICT = 8,
CHECKOUT_ACTION__MAX = 8
};
static int checkout_confirm_update_blob(
checkout_diff_data *data,
const git_diff_delta *delta,
int action)
{
int error;
unsigned int strat = data->opts->checkout_strategy;
struct stat st;
bool update_only = ((strat & GIT_CHECKOUT_UPDATE_ONLY) != 0);
/* for typechange, remove the old item first */
if (delta->status == GIT_DELTA_TYPECHANGE) {
if (update_only)
action = CHECKOUT_ACTION__NONE;
else
action |= CHECKOUT_ACTION__REMOVE;
return action;
}
git_buf_truncate(data->path, data->workdir_len);
if (git_buf_puts(data->path, delta->new_file.path) < 0)
return -1;
if ((error = p_lstat_posixly(git_buf_cstr(data->path), &st)) < 0) {
if (errno == ENOENT) {
if (update_only)
action = CHECKOUT_ACTION__NONE;
} else if (errno == ENOTDIR) {
/* File exists where a parent dir needs to go - i.e. untracked
* typechange. Ignore if UPDATE_ONLY, remove if allowed.
*/
if (update_only)
action = CHECKOUT_ACTION__NONE;
else if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0)
action |= CHECKOUT_ACTION__REMOVE;
else
action = CHECKOUT_ACTION__CONFLICT;
}
/* otherwise let error happen when we attempt blob checkout later */
}
else if (S_ISDIR(st.st_mode)) {
/* Directory exists where a blob needs to go - i.e. untracked
* typechange. Ignore if UPDATE_ONLY, remove if allowed.
*/
if (update_only)
action = CHECKOUT_ACTION__NONE;
else if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) != 0)
action |= CHECKOUT_ACTION__REMOVE;
else
action = CHECKOUT_ACTION__CONFLICT;
}
return action;
}
static int checkout_action_for_delta(
checkout_diff_data *data,
const git_diff_delta *delta,
const git_index_entry *head_entry)
{
int action = CHECKOUT_ACTION__NONE;
unsigned int strat = data->opts->checkout_strategy;
switch (delta->status) {
case GIT_DELTA_UNMODIFIED:
if (!head_entry) {
/* file independently created in wd, even though not in HEAD */
if ((strat & GIT_CHECKOUT_UPDATE_MISSING) == 0)
action = CHECKOUT_ACTION__CONFLICT;
}
else if (!git_oid_equal(&head_entry->oid, &delta->old_file.oid)) {
/* working directory was independently updated to match index */
if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) == 0)
action = CHECKOUT_ACTION__CONFLICT;
}
break;
case GIT_DELTA_ADDED:
/* Impossible. New files should be UNTRACKED or TYPECHANGE */
action = CHECKOUT_ACTION__CONFLICT;
break;
case GIT_DELTA_DELETED:
if (head_entry && /* working dir missing, but exists in HEAD */
(strat & GIT_CHECKOUT_UPDATE_MISSING) == 0)
action = CHECKOUT_ACTION__CONFLICT;
else
action = CHECKOUT_ACTION__UPDATE_BLOB;
break;
case GIT_DELTA_MODIFIED:
case GIT_DELTA_TYPECHANGE:
if (!head_entry) {
/* working dir was independently updated & does not match index */
if ((strat & GIT_CHECKOUT_UPDATE_UNTRACKED) == 0)
action = CHECKOUT_ACTION__CONFLICT;
else
action = CHECKOUT_ACTION__UPDATE_BLOB;
}
else if (git_oid_equal(&head_entry->oid, &delta->new_file.oid))
action = CHECKOUT_ACTION__UPDATE_BLOB;
else if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) == 0)
action = CHECKOUT_ACTION__CONFLICT;
else
action = CHECKOUT_ACTION__UPDATE_BLOB;
break;
case GIT_DELTA_UNTRACKED:
if (!head_entry) {
if ((strat & GIT_CHECKOUT_REMOVE_UNTRACKED) != 0)
action = CHECKOUT_ACTION__REMOVE;
}
else if ((strat & GIT_CHECKOUT_UPDATE_MODIFIED) != 0) {
action = CHECKOUT_ACTION__REMOVE;
} else if ((strat & GIT_CHECKOUT_UPDATE_UNMODIFIED) != 0) {
git_oid wd_oid;
/* if HEAD matches workdir, then remove, else conflict */
if (git_oid_iszero(&delta->new_file.oid) &&
git_diff__oid_for_file(
data->repo, delta->new_file.path, delta->new_file.mode,
delta->new_file.size, &wd_oid) < 0)
action = -1;
else if (git_oid_equal(&head_entry->oid, &wd_oid))
action = CHECKOUT_ACTION__REMOVE;
else
action = CHECKOUT_ACTION__CONFLICT;
} else {
/* present in HEAD and workdir, but absent in index */
action = CHECKOUT_ACTION__CONFLICT;
}
break;
case GIT_DELTA_IGNORED:
default:
/* just skip these files */
break;
}
if (action > 0 && (action & CHECKOUT_ACTION__UPDATE_BLOB) != 0) {
if (S_ISGITLINK(delta->old_file.mode))
action = (action & ~CHECKOUT_ACTION__UPDATE_BLOB) |
CHECKOUT_ACTION__UPDATE_SUBMODULE;
action = checkout_confirm_update_blob(data, delta, action);
}
if (action == CHECKOUT_ACTION__CONFLICT &&
data->opts->conflict_cb != NULL &&
data->opts->conflict_cb(
delta->old_file.path, &delta->old_file.oid,
delta->old_file.mode, delta->new_file.mode,
data->opts->conflict_payload) != 0)
{
giterr_clear();
action = GIT_EUSER;
}
if (action > 0 && (strat & GIT_CHECKOUT_UPDATE_ONLY) != 0)
action = (action & ~CHECKOUT_ACTION__REMOVE);
return action;
}
static int checkout_get_actions(
uint32_t **actions_ptr,
size_t **counts_ptr,
checkout_diff_data *data)
{
int error;
git_diff_list *diff = data->diff;
git_diff_delta *delta;
size_t i, *counts = NULL;
uint32_t *actions = NULL;
git_tree *head = NULL;
git_iterator *hiter = NULL;
char *pfx = git_pathspec_prefix(&data->opts->paths);
const git_index_entry *he;
/* if there is no HEAD, that's okay - we'll make an empty iterator */
(void)git_repository_head_tree(&head, data->repo);
if ((error = git_iterator_for_tree_range(
&hiter, data->repo, head, pfx, pfx)) < 0)
goto fail;
if ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 &&
!hiter->ignore_case &&
(error = git_iterator_spoolandsort(
&hiter, hiter, diff->entrycomp, true)) < 0)
goto fail;
if ((error = git_iterator_current(hiter, &he)) < 0)
goto fail;
git__free(pfx);
pfx = NULL;
*counts_ptr = counts = git__calloc(CHECKOUT_ACTION__MAX+1, sizeof(size_t));
*actions_ptr = actions = git__calloc(diff->deltas.length, sizeof(uint32_t));
if (!counts || !actions) {
error = -1;
goto fail;
}
git_vector_foreach(&diff->deltas, i, delta) {
int cmp = -1, act;
/* try to track HEAD entries parallel to deltas */
while (he) {
cmp = S_ISDIR(delta->new_file.mode) ?
diff->pfxcomp(he->path, delta->new_file.path) :
diff->strcomp(he->path, delta->old_file.path);
if (cmp >= 0)
break;
if (git_iterator_advance(hiter, &he) < 0)
he = NULL;
}
act = checkout_action_for_delta(data, delta, !cmp ? he : NULL);
if (act < 0) {
error = act;
goto fail;
}
if (!cmp && git_iterator_advance(hiter, &he) < 0)
he = NULL;
actions[i] = act;
if (act & CHECKOUT_ACTION__REMOVE)
counts[CHECKOUT_ACTION__REMOVE]++;
if (act & CHECKOUT_ACTION__UPDATE_BLOB)
counts[CHECKOUT_ACTION__UPDATE_BLOB]++;
if (act & CHECKOUT_ACTION__UPDATE_SUBMODULE)
counts[CHECKOUT_ACTION__UPDATE_SUBMODULE]++;
if (act & CHECKOUT_ACTION__CONFLICT)
counts[CHECKOUT_ACTION__CONFLICT]++;
}
if (counts[CHECKOUT_ACTION__CONFLICT] > 0 &&
(data->opts->checkout_strategy & GIT_CHECKOUT_ALLOW_CONFLICTS) == 0)
{
giterr_set(GITERR_CHECKOUT, "%d conflicts prevent checkout",
(int)counts[CHECKOUT_ACTION__CONFLICT]);
goto fail;
}
git_iterator_free(hiter);
git_tree_free(head);
return 0;
fail:
*counts_ptr = NULL;
git__free(counts);
*actions_ptr = NULL;
git__free(actions);
git_iterator_free(hiter);
git_tree_free(head);
git__free(pfx);
return -1;
}
static int checkout_remove_the_old(
git_diff_list *diff,
unsigned int *actions,
checkout_diff_data *data)
{
git_diff_delta *delta;
size_t i;
git_buf_truncate(data->path, data->workdir_len);
git_vector_foreach(&diff->deltas, i, delta) {
if (actions[i] & CHECKOUT_ACTION__REMOVE) {
int error = git_futils_rmdir_r(
delta->new_file.path,
git_buf_cstr(data->path), /* here set to work dir root */
GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS |
GIT_RMDIR_REMOVE_BLOCKERS);
if (error < 0)
return error;
data->completed_steps++;
report_progress(data, delta->new_file.path);
}
}
return 0;
}
static int checkout_create_the_new(
git_diff_list *diff,
unsigned int *actions,
checkout_diff_data *data)
{
git_diff_delta *delta;
size_t i;
git_vector_foreach(&diff->deltas, i, delta) {
if (actions[i] & CHECKOUT_ACTION__UPDATE_BLOB) {
int error = checkout_blob(data, &delta->old_file);
if (error < 0)
return error;
data->completed_steps++;
report_progress(data, delta->old_file.path);
}
}
return 0;
}
static int checkout_create_submodules(
git_diff_list *diff,
unsigned int *actions,
checkout_diff_data *data)
{
git_diff_delta *delta;
size_t i;
git_vector_foreach(&diff->deltas, i, delta) {
if (actions[i] & CHECKOUT_ACTION__UPDATE_SUBMODULE) {
int error = checkout_submodule(data, &delta->old_file);
if (error < 0)
return error;
data->completed_steps++;
report_progress(data, delta->old_file.path);
}
}
return 0;
}
int git_checkout_index(
git_repository *repo,
git_index *index,
git_checkout_opts *opts)
{
git_diff_list *diff = NULL;
git_diff_options diff_opts = {0};
git_checkout_opts checkout_opts;
checkout_diff_data data;
git_buf workdir = GIT_BUF_INIT;
uint32_t *actions = NULL;
size_t *counts = NULL;
int error;
assert(repo);
if ((error = git_repository__ensure_not_bare(repo, "checkout")) < 0)
return error;
diff_opts.flags =
GIT_DIFF_INCLUDE_UNMODIFIED | 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(&diff, repo, index, &diff_opts)) < 0)
goto cleanup;
if ((error = git_buf_puts(&workdir, git_repository_workdir(repo))) < 0)
goto cleanup;
normalize_options(&checkout_opts, opts);
/* Checkout is best performed with up to four passes through the diff.
*
* 0. Figure out what actions should be taken and record for later.
* 1. Next 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.
*/
memset(&data, 0, sizeof(data));
data.path = &workdir;
data.workdir_len = git_buf_len(&workdir);
data.repo = repo;
data.diff = diff;
data.opts = &checkout_opts;
if ((error = checkout_get_actions(&actions, &counts, &data)) < 0)
goto cleanup;
data.total_steps = counts[CHECKOUT_ACTION__REMOVE] +
counts[CHECKOUT_ACTION__UPDATE_BLOB] +
counts[CHECKOUT_ACTION__UPDATE_SUBMODULE];
if ((error = retrieve_symlink_caps(repo, &data.can_symlink)) < 0)
goto cleanup;
report_progress(&data, NULL); /* establish 0 baseline */
if (counts[CHECKOUT_ACTION__REMOVE] > 0 &&
(error = checkout_remove_the_old(diff, actions, &data)) < 0)
goto cleanup;
if (counts[CHECKOUT_ACTION__UPDATE_BLOB] > 0 &&
(error = checkout_create_the_new(diff, actions, &data)) < 0)
goto cleanup;
if (counts[CHECKOUT_ACTION__UPDATE_SUBMODULE] > 0 &&
(error = checkout_create_submodules(diff, actions, &data)) < 0)
goto cleanup;
assert(data.completed_steps == data.total_steps);
cleanup:
if (error == GIT_EUSER)
giterr_clear();
git__free(actions);
git__free(counts);
git_diff_list_free(diff);
git_buf_free(&workdir);
return error;
}
int git_checkout_tree(
git_repository *repo,
const git_object *treeish,
git_checkout_opts *opts)
{
int error = 0;
git_index *index = NULL;
git_tree *tree = NULL;
assert(repo && treeish);
if (git_object_peel((git_object **)&tree, treeish, GIT_OBJ_TREE) < 0) {
giterr_set(
GITERR_CHECKOUT, "Provided object cannot be peeled to a tree");
return -1;
}
/* TODO: create a temp index, load tree there and check it out */
/* load paths in tree that match pathspec into index */
if (!(error = git_repository_index(&index, repo)) &&
!(error = git_index_read_tree_match(
index, tree, opts ? &opts->paths : NULL)) &&
!(error = git_index_write(index)))
error = git_checkout_index(repo, NULL, opts);
git_index_free(index);
git_tree_free(tree);
return error;
}
int git_checkout_head(
git_repository *repo,
git_checkout_opts *opts)
{
int error;
git_reference *head = NULL;
git_object *tree = NULL;
assert(repo);
if (!(error = git_repository_head(&head, repo)) &&
!(error = git_reference_peel(&tree, head, GIT_OBJ_TREE)))
error = git_checkout_tree(repo, tree, opts);
git_reference_free(head);
git_object_free(tree);
return error;
}