Commit 300d192f by Edward Thomson Committed by Edward Thomson

Introduce git_revert to revert a single commit

parent 14984af6
...@@ -117,6 +117,17 @@ GIT_EXTERN(const char *) git_commit_message(const git_commit *commit); ...@@ -117,6 +117,17 @@ GIT_EXTERN(const char *) git_commit_message(const git_commit *commit);
GIT_EXTERN(const char *) git_commit_message_raw(const git_commit *commit); GIT_EXTERN(const char *) git_commit_message_raw(const git_commit *commit);
/** /**
* Get the short "summary" of the git commit message.
*
* The returned message is the summary of the commit, comprising the
* first paragraph of the message with whitespace trimmed and squashed.
*
* @param commit a previously loaded commit.
* @return the summary of a commit or NULL on error
*/
GIT_EXTERN(const char *) git_commit_summary(git_commit *commit);
/**
* Get the commit time (i.e. committer time) of a commit. * Get the commit time (i.e. committer time) of a commit.
* *
* @param commit a previously loaded commit. * @param commit a previously loaded commit.
......
...@@ -71,6 +71,7 @@ typedef enum { ...@@ -71,6 +71,7 @@ typedef enum {
GITERR_MERGE, GITERR_MERGE,
GITERR_SSH, GITERR_SSH,
GITERR_FILTER, GITERR_FILTER,
GITERR_REVERT,
} git_error_t; } git_error_t;
/** /**
......
/*
* 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.
*/
#ifndef INCLUDE_git_revert_h__
#define INCLUDE_git_revert_h__
#include "common.h"
#include "types.h"
#include "merge.h"
/**
* @file git2/revert.h
* @brief Git revert routines
* @defgroup git_revert Git revert routines
* @ingroup Git
* @{
*/
GIT_BEGIN_DECL
typedef struct {
unsigned int version;
/** For merge commits, the "mainline" is treated as the parent. */
unsigned int mainline;
git_merge_tree_opts merge_tree_opts;
git_checkout_opts checkout_opts;
} git_revert_opts;
#define GIT_REVERT_OPTS_VERSION 1
#define GIT_REVERT_OPTS_INIT {GIT_REVERT_OPTS_VERSION, 0, GIT_MERGE_TREE_OPTS_INIT, GIT_CHECKOUT_OPTS_INIT}
/**
* Reverts the given commits, producing changes in the working directory.
*
* @param repo the repository to revert
* @param commits the commits to revert
* @param commits_len the number of commits to revert
* @param flags merge flags
*/
GIT_EXTERN(int) git_revert(
git_repository *repo,
git_commit *commit,
const git_revert_opts *given_opts);
/** @} */
GIT_END_DECL
#endif
...@@ -31,6 +31,7 @@ void git_commit__free(void *_commit) ...@@ -31,6 +31,7 @@ void git_commit__free(void *_commit)
git__free(commit->raw_header); git__free(commit->raw_header);
git__free(commit->raw_message); git__free(commit->raw_message);
git__free(commit->message_encoding); git__free(commit->message_encoding);
git__free(commit->summary);
git__free(commit); git__free(commit);
} }
...@@ -286,6 +287,34 @@ const char *git_commit_message(const git_commit *commit) ...@@ -286,6 +287,34 @@ const char *git_commit_message(const git_commit *commit)
return message; return message;
} }
const char *git_commit_summary(git_commit *commit)
{
git_buf summary = GIT_BUF_INIT;
const char *msg, *space;
assert(commit);
if (!commit->summary) {
for (msg = git_commit_message(commit), space = NULL; *msg; ++msg) {
if (msg[0] == '\n' && (!msg[1] || msg[1] == '\n'))
break;
else if (msg[0] == '\n')
git_buf_putc(&summary, ' ');
else if (git__isspace(msg[0]))
space = space ? space : msg;
else if (space) {
git_buf_put(&summary, space, (msg - space) + 1);
space = NULL;
} else
git_buf_putc(&summary, *msg);
}
commit->summary = git_buf_detach(&summary);
}
return commit->summary;
}
int git_commit_tree(git_tree **tree_out, const git_commit *commit) int git_commit_tree(git_tree **tree_out, const git_commit *commit)
{ {
assert(commit); assert(commit);
......
...@@ -26,6 +26,8 @@ struct git_commit { ...@@ -26,6 +26,8 @@ struct git_commit {
char *message_encoding; char *message_encoding;
char *raw_message; char *raw_message;
char *raw_header; char *raw_header;
char *summary;
}; };
void git_commit__free(void *commit); void git_commit__free(void *commit);
......
...@@ -1206,7 +1206,7 @@ static git_merge_diff *merge_diff_from_index_entries( ...@@ -1206,7 +1206,7 @@ static git_merge_diff *merge_diff_from_index_entries(
/* Merge trees */ /* Merge trees */
static int merge_index_insert_conflict( static int merge_diff_list_insert_conflict(
git_merge_diff_list *diff_list, git_merge_diff_list *diff_list,
struct merge_diff_df_data *merge_df_data, struct merge_diff_df_data *merge_df_data,
const git_index_entry *tree_items[3]) const git_index_entry *tree_items[3])
...@@ -1222,7 +1222,7 @@ static int merge_index_insert_conflict( ...@@ -1222,7 +1222,7 @@ static int merge_index_insert_conflict(
return 0; return 0;
} }
static int merge_index_insert_unmodified( static int merge_diff_list_insert_unmodified(
git_merge_diff_list *diff_list, git_merge_diff_list *diff_list,
const git_index_entry *tree_items[3]) const git_index_entry *tree_items[3])
{ {
...@@ -1252,7 +1252,7 @@ int git_merge_diff_list__find_differences( ...@@ -1252,7 +1252,7 @@ int git_merge_diff_list__find_differences(
size_t i, j; size_t i, j;
int error = 0; int error = 0;
assert(diff_list && our_tree && their_tree); assert(diff_list && (our_tree || their_tree));
if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || if ((error = git_iterator_for_tree(&iterators[TREE_IDX_ANCESTOR], (git_tree *)ancestor_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
(error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 || (error = git_iterator_for_tree(&iterators[TREE_IDX_OURS], (git_tree *)our_tree, GIT_ITERATOR_DONT_IGNORE_CASE, NULL, NULL)) < 0 ||
...@@ -1262,6 +1262,7 @@ int git_merge_diff_list__find_differences( ...@@ -1262,6 +1262,7 @@ int git_merge_diff_list__find_differences(
/* Set up the iterators */ /* Set up the iterators */
for (i = 0; i < 3; i++) { for (i = 0; i < 3; i++) {
error = git_iterator_current(&items[i], iterators[i]); error = git_iterator_current(&items[i], iterators[i]);
if (error < 0 && error != GIT_ITEROVER) if (error < 0 && error != GIT_ITEROVER)
goto done; goto done;
} }
...@@ -1313,9 +1314,9 @@ int git_merge_diff_list__find_differences( ...@@ -1313,9 +1314,9 @@ int git_merge_diff_list__find_differences(
break; break;
if (cur_item_modified) if (cur_item_modified)
error = merge_index_insert_conflict(diff_list, &df_data, cur_items); error = merge_diff_list_insert_conflict(diff_list, &df_data, cur_items);
else else
error = merge_index_insert_unmodified(diff_list, cur_items); error = merge_diff_list_insert_unmodified(diff_list, cur_items);
if (error < 0) if (error < 0)
goto done; goto done;
...@@ -1325,6 +1326,7 @@ int git_merge_diff_list__find_differences( ...@@ -1325,6 +1326,7 @@ int git_merge_diff_list__find_differences(
continue; continue;
error = git_iterator_advance(&items[i], iterators[i]); error = git_iterator_advance(&items[i], iterators[i]);
if (error < 0 && error != GIT_ITEROVER) if (error < 0 && error != GIT_ITEROVER)
goto done; goto done;
} }
...@@ -1569,7 +1571,7 @@ int git_merge_trees( ...@@ -1569,7 +1571,7 @@ int git_merge_trees(
size_t i; size_t i;
int error = 0; int error = 0;
assert(out && repo && our_tree && their_tree); assert(out && repo && (our_tree || their_tree));
*out = NULL; *out = NULL;
...@@ -2268,7 +2270,7 @@ done: ...@@ -2268,7 +2270,7 @@ done:
return error; return error;
} }
static int merge_indexes(git_repository *repo, git_index *index_new) int git_merge__indexes(git_repository *repo, git_index *index_new)
{ {
git_index *index_repo; git_index *index_repo;
unsigned int index_repo_caps; unsigned int index_repo_caps;
...@@ -2440,7 +2442,7 @@ int git_merge( ...@@ -2440,7 +2442,7 @@ int git_merge(
/* TODO: recursive, octopus, etc... */ /* TODO: recursive, octopus, etc... */
if ((error = git_merge_trees(&index_new, repo, ancestor_tree, our_tree, their_trees[0], &opts.merge_tree_opts)) < 0 || if ((error = git_merge_trees(&index_new, repo, ancestor_tree, our_tree, their_trees[0], &opts.merge_tree_opts)) < 0 ||
(error = merge_indexes(repo, index_new)) < 0 || (error = git_merge__indexes(repo, index_new)) < 0 ||
(error = git_repository_index(&index_repo, repo)) < 0 || (error = git_repository_index(&index_repo, repo)) < 0 ||
(error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0) (error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0)
goto on_error; goto on_error;
......
...@@ -158,4 +158,6 @@ int git_merge__setup( ...@@ -158,4 +158,6 @@ int git_merge__setup(
size_t their_heads_len, size_t their_heads_len,
unsigned int flags); unsigned int flags);
int git_merge__indexes(git_repository *repo, git_index *index_new);
#endif #endif
/*
* 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 "filebuf.h"
#include "merge.h"
#include "revert.h"
#include "git2/types.h"
#include "git2/merge.h"
#include "git2/revert.h"
#include "git2/commit.h"
#include "git2/sys/commit.h"
#define GIT_REVERT_FILE_MODE 0666
static int write_revert_head(
git_repository *repo,
const git_commit *commit,
const char *commit_oidstr)
{
git_filebuf file = GIT_FILEBUF_INIT;
git_buf file_path = GIT_BUF_INIT;
int error = 0;
assert(repo && commit);
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_REVERT_HEAD_FILE)) >= 0 &&
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_REVERT_FILE_MODE)) >= 0 &&
(error = git_filebuf_printf(&file, "%s\n", commit_oidstr)) >= 0)
error = git_filebuf_commit(&file);
if (error < 0)
git_filebuf_cleanup(&file);
git_buf_free(&file_path);
return error;
}
static int write_merge_msg(
git_repository *repo,
const git_commit *commit,
const char *commit_oidstr,
const char *commit_msgline)
{
git_filebuf file = GIT_FILEBUF_INIT;
git_buf file_path = GIT_BUF_INIT;
int error = 0;
assert(repo && commit);
if ((error = git_buf_joinpath(&file_path, repo->path_repository, GIT_MERGE_MSG_FILE)) < 0 ||
(error = git_filebuf_open(&file, file_path.ptr, GIT_FILEBUF_FORCE, GIT_REVERT_FILE_MODE)) < 0 ||
(error = git_filebuf_printf(&file, "Revert \"%s\"\n\nThis reverts commit %s.\n",
commit_msgline, commit_oidstr)) < 0)
goto cleanup;
error = git_filebuf_commit(&file);
cleanup:
if (error < 0)
git_filebuf_cleanup(&file);
git_buf_free(&file_path);
return error;
}
static int revert_normalize_opts(
git_repository *repo,
git_revert_opts *opts,
const git_revert_opts *given,
const char *their_label)
{
int error = 0;
unsigned int default_checkout_strategy = GIT_CHECKOUT_SAFE_CREATE |
GIT_CHECKOUT_ALLOW_CONFLICTS;
GIT_UNUSED(repo);
if (given != NULL)
memcpy(opts, given, sizeof(git_revert_opts));
else {
git_revert_opts default_opts = GIT_REVERT_OPTS_INIT;
memcpy(opts, &default_opts, sizeof(git_revert_opts));
}
if (!opts->checkout_opts.checkout_strategy)
opts->checkout_opts.checkout_strategy = default_checkout_strategy;
if (!opts->checkout_opts.our_label)
opts->checkout_opts.our_label = "HEAD";
if (!opts->checkout_opts.their_label)
opts->checkout_opts.their_label = their_label;
return error;
}
int git_revert(
git_repository *repo,
git_commit *commit,
const git_revert_opts *given_opts)
{
git_revert_opts opts;
git_commit *parent_commit = NULL;
git_tree *parent_tree = NULL, *our_tree = NULL, *revert_tree = NULL;
git_index *index_new = NULL, *index_repo = NULL;
char commit_oidstr[GIT_OID_HEXSZ + 1];
const char *commit_msg;
git_buf their_label = GIT_BUF_INIT;
int parent = 0;
int error = 0;
assert(repo && commit);
if ((error = git_repository__ensure_not_bare(repo, "revert")) < 0)
return error;
git_oid_fmt(commit_oidstr, git_commit_id(commit));
commit_oidstr[GIT_OID_HEXSZ] = '\0';
if ((commit_msg = git_commit_summary(commit)) == NULL) {
error = -1;
goto on_error;
}
if ((error = git_buf_printf(&their_label, "parent of %.7s... %s", commit_oidstr, commit_msg)) < 0 ||
(error = revert_normalize_opts(repo, &opts, given_opts, git_buf_cstr(&their_label))) < 0 ||
(error = write_revert_head(repo, commit, commit_oidstr)) < 0 ||
(error = write_merge_msg(repo, commit, commit_oidstr, commit_msg)) < 0 ||
(error = git_repository_head_tree(&our_tree, repo)) < 0 ||
(error = git_commit_tree(&revert_tree, commit)) < 0)
goto on_error;
if (git_commit_parentcount(commit) > 1) {
if (!opts.mainline) {
giterr_set(GITERR_REVERT,
"Mainline branch is not specified but %s is a merge commit",
commit_oidstr);
error = -1;
goto on_error;
}
parent = opts.mainline;
} else {
if (opts.mainline) {
giterr_set(GITERR_REVERT,
"Mainline branch was specified but %s is not a merge",
commit_oidstr);
error = -1;
goto on_error;
}
parent = git_commit_parentcount(commit);
}
if (parent &&
((error = git_commit_parent(&parent_commit, commit, (parent - 1))) < 0 ||
(error = git_commit_tree(&parent_tree, parent_commit)) < 0))
goto on_error;
if ((error = git_merge_trees(&index_new, repo, revert_tree, our_tree, parent_tree, &opts.merge_tree_opts)) < 0 ||
(error = git_merge__indexes(repo, index_new)) < 0 ||
(error = git_repository_index(&index_repo, repo)) < 0 ||
(error = git_checkout_index(repo, index_repo, &opts.checkout_opts)) < 0)
goto on_error;
goto done;
on_error:
git_revert__cleanup(repo);
done:
git_index_free(index_new);
git_index_free(index_repo);
git_tree_free(parent_tree);
git_tree_free(our_tree);
git_tree_free(revert_tree);
git_commit_free(parent_commit);
git_buf_free(&their_label);
return error;
}
int git_revert__cleanup(git_repository *repo)
{
int error = 0;
git_buf revert_head_path = GIT_BUF_INIT,
merge_msg_path = GIT_BUF_INIT;
assert(repo);
if (git_buf_joinpath(&revert_head_path, repo->path_repository, GIT_REVERT_HEAD_FILE) < 0 ||
git_buf_joinpath(&merge_msg_path, repo->path_repository, GIT_MERGE_MSG_FILE) < 0)
return -1;
if (git_path_isfile(revert_head_path.ptr)) {
if ((error = p_unlink(revert_head_path.ptr)) < 0)
goto cleanup;
}
if (git_path_isfile(merge_msg_path.ptr))
(void)p_unlink(merge_msg_path.ptr);
cleanup:
git_buf_free(&merge_msg_path);
git_buf_free(&revert_head_path);
return error;
}
/*
* 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.
*/
#ifndef INCLUDE_revert_h__
#define INCLUDE_revert_h__
#include "git2/repository.h"
int git_revert__cleanup(git_repository *repo);
#endif
#include "clar_libgit2.h" #include "clar_libgit2.h"
#include "commit.h"
#include "git2/commit.h"
static git_repository *_repo; static git_repository *_repo;
...@@ -44,3 +46,30 @@ void test_commit_commit__create_unexisting_update_ref(void) ...@@ -44,3 +46,30 @@ void test_commit_commit__create_unexisting_update_ref(void)
git_signature_free(s); git_signature_free(s);
git_reference_free(ref); git_reference_free(ref);
} }
void assert_commit_summary(const char *expected, const char *given)
{
git_commit *dummy;
cl_assert(dummy = git__calloc(1, sizeof(struct git_commit)));
dummy->raw_message = git__strdup(given);
cl_assert_equal_s(expected, git_commit_summary(dummy));
git_commit__free(dummy);
}
void test_commit_commit__summary(void)
{
assert_commit_summary("One-liner with no trailing newline", "One-liner with no trailing newline");
assert_commit_summary("One-liner with trailing newline", "One-liner with trailing newline\n");
assert_commit_summary("Trimmed leading&trailing newlines", "\n\nTrimmed leading&trailing newlines\n\n");
assert_commit_summary("First paragraph only", "\nFirst paragraph only\n\n(There are more!)");
assert_commit_summary("First paragraph with unwrapped trailing\tlines", "\nFirst paragraph\nwith unwrapped\ntrailing\tlines\n\n(Yes, unwrapped!)");
assert_commit_summary("\tLeading \ttabs", "\tLeading\n\ttabs\n\nis preserved");
assert_commit_summary(" Leading Spaces", " Leading\n Spaces\n\nare preserved");
assert_commit_summary("Trailing tabs\tare removed", "Trailing tabs\tare removed\t\t");
assert_commit_summary("Trailing spaces are removed", "Trailing spaces are removed ");
assert_commit_summary("Trailing tabs", "Trailing tabs\t\n\nare removed");
assert_commit_summary("Trailing spaces", "Trailing spaces \n\nare removed");
}
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
hideDotFiles = dotGitOnly
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
2d440f2b3147d3dc7ad1085813478d6d869d5a4d
5acdc74af27172ec491d213ee36cea7eb9ef2579
13ee9cd5d8e1023c218e0e1ea684ec0c582b5050
52c95c4264245469a0617e289a7d737f156826b4
!File one!
!File one!
File one!
File one
File one
File one
File one
File one
File one
File one
File one!
!File one!
!File one!
!File one!
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File two
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File three
File six, actually!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
File four!
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment