| /* |
| * 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; |
| } |