diff options
| author | Vicent Martà <vicent@github.com> | 2012-10-29 12:57:04 -0700 |
|---|---|---|
| committer | Vicent Martà <vicent@github.com> | 2012-10-29 12:57:04 -0700 |
| commit | 8a1479a55a4f8c0acc7a26fea7865b71fc28b54e (patch) | |
| tree | a2d45540f319b247961d5e8f4d142865ea447f8e /src | |
| parent | a0ce87c51c1a3b1b3b674902148ad28d8e5fa32d (diff) | |
| parent | e4c64cf2aa77a97824db4d2700ca507278ef857d (diff) | |
| download | libgit2-8a1479a55a4f8c0acc7a26fea7865b71fc28b54e.tar.gz | |
Merge pull request #796 from nulltoken/topic/git-stash
Stash
Diffstat (limited to 'src')
| -rw-r--r-- | src/index.c | 5 | ||||
| -rw-r--r-- | src/reflog.c | 22 | ||||
| -rw-r--r-- | src/refs.h | 3 | ||||
| -rw-r--r-- | src/stash.c | 659 |
4 files changed, 681 insertions, 8 deletions
diff --git a/src/index.c b/src/index.c index f92c48df9..38e83d007 100644 --- a/src/index.c +++ b/src/index.c @@ -1071,3 +1071,8 @@ int git_index_read_tree(git_index *index, git_tree *tree) return git_tree_walk(tree, read_tree_cb, GIT_TREEWALK_POST, index); } + +git_repository *git_index_owner(const git_index *index) +{ + return INDEX_OWNER(index); +} diff --git a/src/reflog.c b/src/reflog.c index a1ea7a27d..5d1465eca 100644 --- a/src/reflog.c +++ b/src/reflog.c @@ -166,6 +166,9 @@ void git_reflog_free(git_reflog *reflog) unsigned int i; git_reflog_entry *entry; + if (reflog == NULL) + return; + for (i=0; i < reflog->entries.length; i++) { entry = git_vector_get(&reflog->entries, i); @@ -185,7 +188,10 @@ static int retrieve_reflog_path(git_buf *path, git_reference *ref) static int create_new_reflog_file(const char *filepath) { - int fd; + int fd, error; + + if ((error = git_futils_mkpath2file(filepath, GIT_REFLOG_DIR_MODE)) < 0) + return error; if ((fd = p_open(filepath, O_WRONLY | O_CREAT | O_TRUNC, @@ -463,26 +469,26 @@ int git_reflog_drop( if (!rewrite_previous_entry) return 0; - /* No need to rewrite anything when removing the first entry */ - if (idx == 0) + /* No need to rewrite anything when removing the most recent entry */ + if (idx == entrycount - 1) return 0; /* There are no more entries in the log */ if (entrycount == 1) return 0; - entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx - 1); + entry = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); - /* If the last entry has just been removed... */ - if (idx == entrycount - 1) { - /* ...clear the oid_old member of the "new" last entry */ + /* If the oldest entry has just been removed... */ + if (idx == 0) { + /* ...clear the oid_old member of the "new" oldest entry */ if (git_oid_fromstr(&entry->oid_old, GIT_OID_HEX_ZERO) < 0) return -1; return 0; } - previous = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx); + previous = (git_reflog_entry *)git_reflog_entry_byindex(reflog, idx - 1); git_oid_cpy(&entry->oid_old, &previous->oid_cur); return 0; diff --git a/src/refs.h b/src/refs.h index 58e2fd558..7b6f9611a 100644 --- a/src/refs.h +++ b/src/refs.h @@ -35,6 +35,9 @@ #define GIT_CHERRY_PICK_HEAD_FILE "CHERRY_PICK_HEAD" #define GIT_REFS_HEADS_MASTER_FILE GIT_REFS_HEADS_DIR "master" +#define GIT_STASH_FILE "stash" +#define GIT_REFS_STASH_FILE GIT_REFS_DIR GIT_STASH_FILE + #define GIT_REFNAME_MAX 1024 struct git_reference { diff --git a/src/stash.c b/src/stash.c new file mode 100644 index 000000000..3011f00f0 --- /dev/null +++ b/src/stash.c @@ -0,0 +1,659 @@ +/* + * 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" + +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; + int 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_oid(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_tree_create_fromindex(&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( + void *cb_data, + const git_diff_delta *delta, + float progress) +{ + int pos; + struct cb_data *data = (struct cb_data *)cb_data; + + GIT_UNUSED(progress); + + switch (delta->status) { + case GIT_DELTA_IGNORED: + if (!data->include_ignored) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + case GIT_DELTA_UNTRACKED: + if (!data->include_untracked) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + case GIT_DELTA_ADDED: + /* Fall through */ + case GIT_DELTA_MODIFIED: + if (!data->include_changed) + break; + + return git_index_add(data->index, delta->new_file.path, 0); + + 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, pos) < 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 = {0}; + 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_workdir_to_tree(git_index_owner(index), &opts, i_tree, &diff) < 0) + goto cleanup; + + if (git_diff_foreach(diff, &data, update_index_cb, NULL, NULL) < 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_tree *b_tree = NULL; + git_diff_list *diff = NULL, *diff2 = NULL; + git_diff_options opts = {0}; + struct cb_data data = {0}; + int error = -1; + + if (git_commit_tree(&b_tree, b_commit) < 0) + goto cleanup; + + if (git_diff_index_to_tree(git_index_owner(index), &opts, b_tree, &diff) < 0) + goto cleanup; + + if (git_diff_workdir_to_index(git_index_owner(index), &opts, &diff2) < 0) + goto cleanup; + + if (git_diff_merge(diff, diff2) < 0) + goto cleanup; + + data.index = index; + data.include_changed = true; + + if (git_diff_foreach(diff, &data, update_index_cb, NULL, NULL) < 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_oid(&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; + + memset(&opts, 0, sizeof(opts)); + 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; + + memset(&opts, 0, sizeof(git_checkout_opts)); + + opts.checkout_strategy = + GIT_CHECKOUT_CREATE_MISSING | GIT_CHECKOUT_OVERWRITE_MODIFIED; + + 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, + 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, max - i - 1); + + if (callback(i, + git_reflog_entry_msg(entry), + git_reflog_entry_oidnew(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, max - index - 1, 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; +} |
