summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorVicent Martí <vicent@github.com>2012-10-29 12:57:04 -0700
committerVicent Martí <vicent@github.com>2012-10-29 12:57:04 -0700
commit8a1479a55a4f8c0acc7a26fea7865b71fc28b54e (patch)
treea2d45540f319b247961d5e8f4d142865ea447f8e /src
parenta0ce87c51c1a3b1b3b674902148ad28d8e5fa32d (diff)
parente4c64cf2aa77a97824db4d2700ca507278ef857d (diff)
downloadlibgit2-8a1479a55a4f8c0acc7a26fea7865b71fc28b54e.tar.gz
Merge pull request #796 from nulltoken/topic/git-stash
Stash
Diffstat (limited to 'src')
-rw-r--r--src/index.c5
-rw-r--r--src/reflog.c22
-rw-r--r--src/refs.h3
-rw-r--r--src/stash.c659
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;
+}