mirror of
https://git.proxmox.com/git/libgit2
synced 2025-05-06 09:41:04 +00:00

This is a significant reorganization of the diff code to break it into a set of more clearly distinct files and to document the new organization. Hopefully this will make the diff code easier to understand and to extend. This adds a new `git_diff_driver` object that looks of diff driver information from the attributes and the config so that things like function content in diff headers can be provided. The full driver spec is not implemented in the commit - this is focused on the reorganization of the code and putting the driver hooks in place. This also removes a few #includes from src/repository.h that were overbroad, but as a result required extra #includes in a variety of places since including src/repository.h no longer results in pulling in the whole world.
667 lines
14 KiB
C
667 lines
14 KiB
C
/*
|
|
* Copyright (C) the libgit2 contributors. All rights reserved.
|
|
*
|
|
* 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 "git2/index.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 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;
|
|
|
|
if (strcmp("HEAD", git_reference_name(head)) == 0)
|
|
error = git_buf_puts(stash_message, "(no branch): ");
|
|
else
|
|
error = git_buf_printf(
|
|
stash_message,
|
|
"%s: ",
|
|
git_reference_name(head) + strlen(GIT_REFS_HEADS_DIR));
|
|
if (error < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_commit_lookup(
|
|
b_commit, repo, git_reference_target(head))) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = append_commit_description(stash_message, *b_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
cleanup:
|
|
git_reference_free(head);
|
|
return error;
|
|
}
|
|
|
|
static int build_tree_from_index(git_tree **out, git_index *index)
|
|
{
|
|
int error;
|
|
git_oid i_tree_oid;
|
|
|
|
if ((error = 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;
|
|
|
|
if ((error = build_tree_from_index(&i_tree, index)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_buf_printf(&msg, "index on %s\n", message)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = 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;
|
|
|
|
int error;
|
|
|
|
bool include_changed;
|
|
bool include_untracked;
|
|
bool include_ignored;
|
|
};
|
|
|
|
static int update_index_cb(
|
|
const git_diff_delta *delta,
|
|
float progress,
|
|
void *payload)
|
|
{
|
|
struct cb_data *data = (struct cb_data *)payload;
|
|
const char *add_path = NULL;
|
|
|
|
GIT_UNUSED(progress);
|
|
|
|
switch (delta->status) {
|
|
case GIT_DELTA_IGNORED:
|
|
if (data->include_ignored)
|
|
add_path = delta->new_file.path;
|
|
break;
|
|
|
|
case GIT_DELTA_UNTRACKED:
|
|
if (data->include_untracked)
|
|
add_path = delta->new_file.path;
|
|
break;
|
|
|
|
case GIT_DELTA_ADDED:
|
|
case GIT_DELTA_MODIFIED:
|
|
if (data->include_changed)
|
|
add_path = delta->new_file.path;
|
|
break;
|
|
|
|
case GIT_DELTA_DELETED:
|
|
if (!data->include_changed)
|
|
break;
|
|
if (git_index_find(NULL, data->index, delta->old_file.path) == 0)
|
|
data->error = git_index_remove(
|
|
data->index, delta->old_file.path, 0);
|
|
break;
|
|
|
|
default:
|
|
/* Unimplemented */
|
|
giterr_set(
|
|
GITERR_INVALID,
|
|
"Cannot update index. Unimplemented status (%d)",
|
|
delta->status);
|
|
data->error = -1;
|
|
break;
|
|
}
|
|
|
|
if (add_path != NULL)
|
|
data->error = git_index_add_bypath(data->index, add_path);
|
|
|
|
return data->error;
|
|
}
|
|
|
|
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;
|
|
|
|
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 ((error = git_commit_tree(&i_tree, i_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_diff_tree_to_workdir(
|
|
&diff, git_index_owner(index), i_tree, &opts)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_diff_foreach(
|
|
diff, update_index_cb, NULL, NULL, &data)) < 0)
|
|
{
|
|
if (error == GIT_EUSER)
|
|
error = data.error;
|
|
goto cleanup;
|
|
}
|
|
|
|
error = build_tree_from_index(tree_out, index);
|
|
|
|
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;
|
|
|
|
if ((error = build_untracked_tree(&u_tree, index, i_commit, flags)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_buf_printf(&msg, "untracked files on %s\n", message)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = 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;
|
|
|
|
if ((error = git_commit_tree(&b_tree, b_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_diff_tree_to_index(&diff, repo, b_tree, NULL, &opts)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_diff_index_to_workdir(&diff2, repo, NULL, &opts)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_diff_merge(diff, diff2)) < 0)
|
|
goto cleanup;
|
|
|
|
data.index = index;
|
|
data.include_changed = true;
|
|
|
|
if ((error = git_diff_foreach(
|
|
diff, update_index_cb, NULL, NULL, &data)) < 0)
|
|
{
|
|
if (error == GIT_EUSER)
|
|
error = data.error;
|
|
goto cleanup;
|
|
}
|
|
|
|
|
|
if ((error = build_tree_from_index(tree_out, index)) < 0)
|
|
goto cleanup;
|
|
|
|
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)
|
|
{
|
|
int error = 0;
|
|
git_tree *w_tree = NULL, *i_tree = NULL;
|
|
const git_commit *parents[] = { NULL, NULL, NULL };
|
|
|
|
parents[0] = b_commit;
|
|
parents[1] = i_commit;
|
|
parents[2] = u_commit;
|
|
|
|
if ((error = git_commit_tree(&i_tree, i_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_index_read_tree(index, i_tree)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = build_workdir_tree(&w_tree, index, b_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
error = git_commit_create(
|
|
w_commit_oid,
|
|
git_index_owner(index),
|
|
NULL,
|
|
stasher,
|
|
stasher,
|
|
NULL,
|
|
message,
|
|
w_tree,
|
|
u_commit ? 3 : 2,
|
|
parents);
|
|
|
|
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;
|
|
|
|
if ((error = git_buf_set(&buf, git_buf_cstr(msg), git_buf_len(msg))) < 0)
|
|
return error;
|
|
|
|
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;
|
|
|
|
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_FORCE;
|
|
|
|
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 = git_repository__ensure_not_bare(repo, "stash save")) < 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) != 0,
|
|
(flags & GIT_STASH_INCLUDE_IGNORED) != 0)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = git_repository_index(&index, repo)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = commit_index(
|
|
&i_commit, index, stasher, git_buf_cstr(&msg), b_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((flags & (GIT_STASH_INCLUDE_UNTRACKED | GIT_STASH_INCLUDE_IGNORED)) &&
|
|
(error = commit_untracked(
|
|
&u_commit, index, stasher, git_buf_cstr(&msg),
|
|
i_commit, flags)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = prepare_worktree_commit_message(&msg, message)) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = commit_worktree(
|
|
out, index, stasher, git_buf_cstr(&msg),
|
|
i_commit, b_commit, u_commit)) < 0)
|
|
goto cleanup;
|
|
|
|
git_buf_rtrim(&msg);
|
|
|
|
if ((error = update_reflog(out, repo, stasher, git_buf_cstr(&msg))) < 0)
|
|
goto cleanup;
|
|
|
|
if ((error = reset_index_and_workdir(
|
|
repo,
|
|
((flags & GIT_STASH_KEEP_INDEX) != 0) ? i_commit : b_commit,
|
|
(flags & GIT_STASH_INCLUDE_UNTRACKED) != 0)) < 0)
|
|
goto cleanup;
|
|
|
|
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) {
|
|
giterr_clear();
|
|
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;
|
|
break;
|
|
}
|
|
}
|
|
|
|
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);
|
|
git_reference_free(stash);
|
|
stash = NULL;
|
|
} else if (index == 0) {
|
|
const git_reflog_entry *entry;
|
|
|
|
entry = git_reflog_entry_byindex(reflog, 0);
|
|
|
|
git_reference_free(stash);
|
|
error = git_reference_create(&stash, repo, GIT_REFS_STASH_FILE, &entry->oid_cur, 1);
|
|
}
|
|
|
|
cleanup:
|
|
git_reference_free(stash);
|
|
git_reflog_free(reflog);
|
|
return error;
|
|
}
|