mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-07 16:22:40 +00:00

An earlier change to `git_diff_from_iterators` introduced a memory leak where the allocated spoolandsort iterator was not returned to the caller and thus not freed. One proposal changes all iterator APIs to use git_iterator** so we can reallocate the iterator at will, but that seems unexpected. This commit makes it so that an iterator can be changed in place. The callbacks are isolated in a separate structure and a pointer to that structure can be reassigned by the spoolandsort extension. This means that spoolandsort doesn't create a new iterator; it just allocates a new block of callbacks (along with space for its own extra data) and swaps that into the iterator. Additionally, since spoolandsort is only needed to switch the case sensitivity of an iterator, this simplifies the API to only take the ignore_case boolean and to be a no-op if the iterator already matches the requested case sensitivity.
748 lines
18 KiB
C
748 lines
18 KiB
C
/*
|
|
* 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 *out)
|
|
{
|
|
git_config *cfg;
|
|
int can_symlink = 0;
|
|
int error;
|
|
|
|
if (git_repository_config__weakptr(&cfg, repo) < 0)
|
|
return -1;
|
|
|
|
error = git_config_get_bool(&can_symlink, cfg, "core.symlinks");
|
|
|
|
/* If "core.symlinks" is not found anywhere, default to true. */
|
|
if (error == GIT_ENOTFOUND) {
|
|
can_symlink = true;
|
|
error = 0;
|
|
}
|
|
|
|
if (error >= 0)
|
|
*out = can_symlink;
|
|
|
|
return error;
|
|
}
|
|
|
|
static void normalize_options(
|
|
git_checkout_opts *normalized, git_checkout_opts *proposed)
|
|
{
|
|
assert(normalized);
|
|
|
|
if (!proposed)
|
|
GIT_INIT_STRUCTURE(normalized, GIT_CHECKOUT_OPTS_VERSION);
|
|
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 */
|
|
if (((error = git_repository_head_tree(&head, data->repo)) < 0) &&
|
|
!(error == GIT_ENOTFOUND || error == GIT_EORPHANEDHEAD))
|
|
return -1;
|
|
|
|
if ((error = git_iterator_for_tree_range(&hiter, head, pfx, pfx)) < 0)
|
|
goto fail;
|
|
|
|
if ((diff->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 &&
|
|
(error = git_iterator_spoolandsort_push(hiter, 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 = GIT_DIFF_OPTIONS_INIT;
|
|
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);
|
|
|
|
GITERR_CHECK_VERSION(opts, GIT_CHECKOUT_OPTS_VERSION, "git_checkout_opts");
|
|
|
|
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_index_to_workdir(&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;
|
|
}
|