libgit2/src/stash.c
Russell Belfer 56c72b759c Fix diff constructor name order confusion
The diff constructor functions had some confusing names, where the
"old" side of the diff was coming after the "new" side.  This
reverses the order in the function name to make it less confusing.

Specifically...

* git_diff_index_to_tree becomes git_diff_tree_to_index
* git_diff_workdir_to_index becomes git_diff_index_to_workdir
* git_diff_workdir_to_tree becomes git_diff_tree_to_workdir
2012-12-17 11:00:53 -08:00

659 lines
14 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 "common.h"
#include "repository.h"
#include "commit.h"
#include "tree.h"
#include "reflog.h"
#include "git2/diff.h"
#include "git2/stash.h"
#include "git2/status.h"
#include "git2/checkout.h"
#include "signature.h"
static int create_error(int error, const char *msg)
{
giterr_set(GITERR_STASH, "Cannot stash changes - %s", msg);
return error;
}
static int ensure_non_bare_repository(git_repository *repo)
{
if (!git_repository_is_bare(repo))
return 0;
return create_error(GIT_EBAREREPO,
"Stash related operations require a working directory.");
}
static int retrieve_head(git_reference **out, git_repository *repo)
{
int error = git_repository_head(out, repo);
if (error == GIT_EORPHANEDHEAD)
return create_error(error, "You do not have the initial commit yet.");
return error;
}
static int append_abbreviated_oid(git_buf *out, const git_oid *b_commit)
{
char *formatted_oid;
formatted_oid = git_oid_allocfmt(b_commit);
GITERR_CHECK_ALLOC(formatted_oid);
git_buf_put(out, formatted_oid, 7);
git__free(formatted_oid);
return git_buf_oom(out) ? -1 : 0;
}
static int append_commit_description(git_buf *out, git_commit* commit)
{
const char *message;
size_t pos = 0, len;
if (append_abbreviated_oid(out, git_commit_id(commit)) < 0)
return -1;
message = git_commit_message(commit);
len = strlen(message);
/* TODO: Replace with proper commit short message
* when git_commit_message_short() is implemented.
*/
while (pos < len && message[pos] != '\n')
pos++;
git_buf_putc(out, ' ');
git_buf_put(out, message, pos);
git_buf_putc(out, '\n');
return git_buf_oom(out) ? -1 : 0;
}
static int retrieve_base_commit_and_message(
git_commit **b_commit,
git_buf *stash_message,
git_repository *repo)
{
git_reference *head = NULL;
int error;
if ((error = retrieve_head(&head, repo)) < 0)
return error;
error = -1;
if (strcmp("HEAD", git_reference_name(head)) == 0)
git_buf_puts(stash_message, "(no branch): ");
else
git_buf_printf(
stash_message,
"%s: ",
git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR));
if (git_commit_lookup(b_commit, repo, git_reference_target(head)) < 0)
goto cleanup;
if (append_commit_description(stash_message, *b_commit) < 0)
goto cleanup;
error = 0;
cleanup:
git_reference_free(head);
return error;
}
static int build_tree_from_index(git_tree **out, git_index *index)
{
git_oid i_tree_oid;
if (git_index_write_tree(&i_tree_oid, index) < 0)
return -1;
return git_tree_lookup(out, git_index_owner(index), &i_tree_oid);
}
static int commit_index(
git_commit **i_commit,
git_index *index,
git_signature *stasher,
const char *message,
const git_commit *parent)
{
git_tree *i_tree = NULL;
git_oid i_commit_oid;
git_buf msg = GIT_BUF_INIT;
int error = -1;
if (build_tree_from_index(&i_tree, index) < 0)
goto cleanup;
if (git_buf_printf(&msg, "index on %s\n", message) < 0)
goto cleanup;
if (git_commit_create(
&i_commit_oid,
git_index_owner(index),
NULL,
stasher,
stasher,
NULL,
git_buf_cstr(&msg),
i_tree,
1,
&parent) < 0)
goto cleanup;
error = git_commit_lookup(i_commit, git_index_owner(index), &i_commit_oid);
cleanup:
git_tree_free(i_tree);
git_buf_free(&msg);
return error;
}
struct cb_data {
git_index *index;
bool include_changed;
bool include_untracked;
bool include_ignored;
};
static int update_index_cb(
const git_diff_delta *delta,
float progress,
void *payload)
{
int pos;
struct cb_data *data = (struct cb_data *)payload;
GIT_UNUSED(progress);
switch (delta->status) {
case GIT_DELTA_IGNORED:
if (!data->include_ignored)
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_UNTRACKED:
if (!data->include_untracked)
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_ADDED:
/* Fall through */
case GIT_DELTA_MODIFIED:
if (!data->include_changed)
break;
return git_index_add_from_workdir(data->index, delta->new_file.path);
case GIT_DELTA_DELETED:
if (!data->include_changed)
break;
if ((pos = git_index_find(data->index, delta->new_file.path)) < 0)
return -1;
if (git_index_remove(data->index, delta->new_file.path, 0) < 0)
return -1;
default:
/* Unimplemented */
giterr_set(
GITERR_INVALID,
"Cannot update index. Unimplemented status kind (%d)",
delta->status);
return -1;
}
return 0;
}
static int build_untracked_tree(
git_tree **tree_out,
git_index *index,
git_commit *i_commit,
uint32_t flags)
{
git_tree *i_tree = NULL;
git_diff_list *diff = NULL;
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
struct cb_data data = {0};
int error = -1;
git_index_clear(index);
data.index = index;
if (flags & GIT_STASH_INCLUDE_UNTRACKED) {
opts.flags |= GIT_DIFF_INCLUDE_UNTRACKED | GIT_DIFF_RECURSE_UNTRACKED_DIRS;
data.include_untracked = true;
}
if (flags & GIT_STASH_INCLUDE_IGNORED) {
opts.flags |= GIT_DIFF_INCLUDE_IGNORED;
data.include_ignored = true;
}
if (git_commit_tree(&i_tree, i_commit) < 0)
goto cleanup;
if (git_diff_tree_to_workdir(&diff, git_index_owner(index), i_tree, &opts) < 0)
goto cleanup;
if (git_diff_foreach(diff, update_index_cb, NULL, NULL, &data) < 0)
goto cleanup;
if (build_tree_from_index(tree_out, index) < 0)
goto cleanup;
error = 0;
cleanup:
git_diff_list_free(diff);
git_tree_free(i_tree);
return error;
}
static int commit_untracked(
git_commit **u_commit,
git_index *index,
git_signature *stasher,
const char *message,
git_commit *i_commit,
uint32_t flags)
{
git_tree *u_tree = NULL;
git_oid u_commit_oid;
git_buf msg = GIT_BUF_INIT;
int error = -1;
if (build_untracked_tree(&u_tree, index, i_commit, flags) < 0)
goto cleanup;
if (git_buf_printf(&msg, "untracked files on %s\n", message) < 0)
goto cleanup;
if (git_commit_create(
&u_commit_oid,
git_index_owner(index),
NULL,
stasher,
stasher,
NULL,
git_buf_cstr(&msg),
u_tree,
0,
NULL) < 0)
goto cleanup;
error = git_commit_lookup(u_commit, git_index_owner(index), &u_commit_oid);
cleanup:
git_tree_free(u_tree);
git_buf_free(&msg);
return error;
}
static int build_workdir_tree(
git_tree **tree_out,
git_index *index,
git_commit *b_commit)
{
git_repository *repo = git_index_owner(index);
git_tree *b_tree = NULL;
git_diff_list *diff = NULL, *diff2 = NULL;
git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
struct cb_data data = {0};
int error = -1;
if (git_commit_tree(&b_tree, b_commit) < 0)
goto cleanup;
if (git_diff_tree_to_index(&diff, repo, b_tree, NULL, &opts) < 0)
goto cleanup;
if (git_diff_index_to_workdir(&diff2, repo, NULL, &opts) < 0)
goto cleanup;
if (git_diff_merge(diff, diff2) < 0)
goto cleanup;
data.index = index;
data.include_changed = true;
if (git_diff_foreach(diff, update_index_cb, NULL, NULL, &data) < 0)
goto cleanup;
if (build_tree_from_index(tree_out, index) < 0)
goto cleanup;
error = 0;
cleanup:
git_diff_list_free(diff);
git_diff_list_free(diff2);
git_tree_free(b_tree);
return error;
}
static int commit_worktree(
git_oid *w_commit_oid,
git_index *index,
git_signature *stasher,
const char *message,
git_commit *i_commit,
git_commit *b_commit,
git_commit *u_commit)
{
git_tree *w_tree = NULL, *i_tree = NULL;
int error = -1;
const git_commit *parents[] = { NULL, NULL, NULL };
parents[0] = b_commit;
parents[1] = i_commit;
parents[2] = u_commit;
if (git_commit_tree(&i_tree, i_commit) < 0)
return -1;
if (git_index_read_tree(index, i_tree) < 0)
goto cleanup;
if (build_workdir_tree(&w_tree, index, b_commit) < 0)
goto cleanup;
if (git_commit_create(
w_commit_oid,
git_index_owner(index),
NULL,
stasher,
stasher,
NULL,
message,
w_tree,
u_commit ? 3 : 2, parents) < 0)
goto cleanup;
error = 0;
cleanup:
git_tree_free(i_tree);
git_tree_free(w_tree);
return error;
}
static int prepare_worktree_commit_message(
git_buf* msg,
const char *user_message)
{
git_buf buf = GIT_BUF_INIT;
int error = -1;
git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg));
git_buf_clear(msg);
if (!user_message)
git_buf_printf(msg, "WIP on %s", git_buf_cstr(&buf));
else {
const char *colon;
if ((colon = strchr(git_buf_cstr(&buf), ':')) == NULL)
goto cleanup;
git_buf_puts(msg, "On ");
git_buf_put(msg, git_buf_cstr(&buf), colon - buf.ptr);
git_buf_printf(msg, ": %s\n", user_message);
}
error = git_buf_oom(msg) || git_buf_oom(&buf) ? -1 : 0;
cleanup:
git_buf_free(&buf);
return error;
}
static int update_reflog(
git_oid *w_commit_oid,
git_repository *repo,
git_signature *stasher,
const char *message)
{
git_reference *stash = NULL;
git_reflog *reflog = NULL;
int error;
if ((error = git_reference_create(&stash, repo, GIT_REFS_STASH_FILE, w_commit_oid, 1)) < 0)
goto cleanup;
if ((error = git_reflog_read(&reflog, stash)) < 0)
goto cleanup;
if ((error = git_reflog_append(reflog, w_commit_oid, stasher, message)) < 0)
goto cleanup;
if ((error = git_reflog_write(reflog)) < 0)
goto cleanup;
error = 0;
cleanup:
git_reference_free(stash);
git_reflog_free(reflog);
return error;
}
static int is_dirty_cb(const char *path, unsigned int status, void *payload)
{
GIT_UNUSED(path);
GIT_UNUSED(status);
GIT_UNUSED(payload);
return 1;
}
static int ensure_there_are_changes_to_stash(
git_repository *repo,
bool include_untracked_files,
bool include_ignored_files)
{
int error;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
if (include_untracked_files)
opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED |
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
if (include_ignored_files)
opts.flags = GIT_STATUS_OPT_INCLUDE_IGNORED;
error = git_status_foreach_ext(repo, &opts, is_dirty_cb, NULL);
if (error == GIT_EUSER)
return 0;
if (!error)
return create_error(GIT_ENOTFOUND, "There is nothing to stash.");
return error;
}
static int reset_index_and_workdir(
git_repository *repo,
git_commit *commit,
bool remove_untracked)
{
git_checkout_opts opts = GIT_CHECKOUT_OPTS_INIT;
opts.checkout_strategy =
GIT_CHECKOUT_UPDATE_MODIFIED | GIT_CHECKOUT_UPDATE_UNTRACKED;
if (remove_untracked)
opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED;
return git_checkout_tree(repo, (git_object *)commit, &opts);
}
int git_stash_save(
git_oid *out,
git_repository *repo,
git_signature *stasher,
const char *message,
uint32_t flags)
{
git_index *index = NULL;
git_commit *b_commit = NULL, *i_commit = NULL, *u_commit = NULL;
git_buf msg = GIT_BUF_INIT;
int error;
assert(out && repo && stasher);
if ((error = ensure_non_bare_repository(repo)) < 0)
return error;
if ((error = retrieve_base_commit_and_message(&b_commit, &msg, repo)) < 0)
goto cleanup;
if ((error = ensure_there_are_changes_to_stash(
repo,
(flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED,
(flags & GIT_STASH_INCLUDE_IGNORED) == GIT_STASH_INCLUDE_IGNORED)) < 0)
goto cleanup;
error = -1;
if (git_repository_index(&index, repo) < 0)
goto cleanup;
if (commit_index(&i_commit, index, stasher, git_buf_cstr(&msg), b_commit) < 0)
goto cleanup;
if ((flags & GIT_STASH_INCLUDE_UNTRACKED || flags & GIT_STASH_INCLUDE_IGNORED)
&& commit_untracked(&u_commit, index, stasher, git_buf_cstr(&msg), i_commit, flags) < 0)
goto cleanup;
if (prepare_worktree_commit_message(&msg, message) < 0)
goto cleanup;
if (commit_worktree(out, index, stasher, git_buf_cstr(&msg), i_commit, b_commit, u_commit) < 0)
goto cleanup;
git_buf_rtrim(&msg);
if (update_reflog(out, repo, stasher, git_buf_cstr(&msg)) < 0)
goto cleanup;
if (reset_index_and_workdir(
repo,
((flags & GIT_STASH_KEEP_INDEX) == GIT_STASH_KEEP_INDEX) ?
i_commit : b_commit,
(flags & GIT_STASH_INCLUDE_UNTRACKED) == GIT_STASH_INCLUDE_UNTRACKED) < 0)
goto cleanup;
error = 0;
cleanup:
git_buf_free(&msg);
git_commit_free(i_commit);
git_commit_free(b_commit);
git_commit_free(u_commit);
git_index_free(index);
return error;
}
int git_stash_foreach(
git_repository *repo,
git_stash_cb callback,
void *payload)
{
git_reference *stash;
git_reflog *reflog = NULL;
int error;
size_t i, max;
const git_reflog_entry *entry;
error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE);
if (error == GIT_ENOTFOUND)
return 0;
if (error < 0)
goto cleanup;
if ((error = git_reflog_read(&reflog, stash)) < 0)
goto cleanup;
max = git_reflog_entrycount(reflog);
for (i = 0; i < max; i++) {
entry = git_reflog_entry_byindex(reflog, i);
if (callback(i,
git_reflog_entry_message(entry),
git_reflog_entry_id_new(entry),
payload)) {
error = GIT_EUSER;
goto cleanup;
}
}
error = 0;
cleanup:
git_reference_free(stash);
git_reflog_free(reflog);
return error;
}
int git_stash_drop(
git_repository *repo,
size_t index)
{
git_reference *stash;
git_reflog *reflog = NULL;
size_t max;
int error;
if ((error = git_reference_lookup(&stash, repo, GIT_REFS_STASH_FILE)) < 0)
return error;
if ((error = git_reflog_read(&reflog, stash)) < 0)
goto cleanup;
max = git_reflog_entrycount(reflog);
if (index > max - 1) {
error = GIT_ENOTFOUND;
giterr_set(GITERR_STASH, "No stashed state at position %" PRIuZ, index);
goto cleanup;
}
if ((error = git_reflog_drop(reflog, index, true)) < 0)
goto cleanup;
if ((error = git_reflog_write(reflog)) < 0)
goto cleanup;
if (max == 1) {
error = git_reference_delete(stash);
stash = NULL;
}
cleanup:
git_reference_free(stash);
git_reflog_free(reflog);
return error;
}