Commit dfe8c8df by Edward Thomson Committed by Russell Belfer

handle renames in status computation

parent 1ee2ef87
...@@ -42,6 +42,7 @@ typedef enum { ...@@ -42,6 +42,7 @@ typedef enum {
GIT_STATUS_WT_MODIFIED = (1u << 8), GIT_STATUS_WT_MODIFIED = (1u << 8),
GIT_STATUS_WT_DELETED = (1u << 9), GIT_STATUS_WT_DELETED = (1u << 9),
GIT_STATUS_WT_TYPECHANGE = (1u << 10), GIT_STATUS_WT_TYPECHANGE = (1u << 10),
GIT_STATUS_WT_RENAMED = (1u << 11),
GIT_STATUS_IGNORED = (1u << 14), GIT_STATUS_IGNORED = (1u << 14),
} git_status_t; } git_status_t;
...@@ -130,6 +131,10 @@ typedef enum { ...@@ -130,6 +131,10 @@ typedef enum {
* - GIT_STATUS_OPT_RECURSE_IGNORED_DIRS indicates that the contents of * - GIT_STATUS_OPT_RECURSE_IGNORED_DIRS indicates that the contents of
* ignored directories should be included in the status. This is like * ignored directories should be included in the status. This is like
* doing `git ls-files -o -i --exclude-standard` with core git. * doing `git ls-files -o -i --exclude-standard` with core git.
* - GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX indicates that items that are
* renamed in the index will be reported as renames.
* - GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR indicates that items that
* are renamed in the working directory will be reported as renames.
* *
* Calling `git_status_foreach()` is like calling the extended version * Calling `git_status_foreach()` is like calling the extended version
* with: GIT_STATUS_OPT_INCLUDE_IGNORED, GIT_STATUS_OPT_INCLUDE_UNTRACKED, * with: GIT_STATUS_OPT_INCLUDE_IGNORED, GIT_STATUS_OPT_INCLUDE_UNTRACKED,
...@@ -144,6 +149,8 @@ typedef enum { ...@@ -144,6 +149,8 @@ typedef enum {
GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS = (1u << 4), GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS = (1u << 4),
GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH = (1u << 5), GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH = (1u << 5),
GIT_STATUS_OPT_RECURSE_IGNORED_DIRS = (1u << 6), GIT_STATUS_OPT_RECURSE_IGNORED_DIRS = (1u << 6),
GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX = (1u << 7),
GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR = (1u << 8),
} git_status_opt_t; } git_status_opt_t;
#define GIT_STATUS_OPT_DEFAULTS \ #define GIT_STATUS_OPT_DEFAULTS \
......
...@@ -54,7 +54,6 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) ...@@ -54,7 +54,6 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status)
switch (workdir_status) { switch (workdir_status) {
case GIT_DELTA_ADDED: case GIT_DELTA_ADDED:
case GIT_DELTA_RENAMED:
case GIT_DELTA_COPIED: case GIT_DELTA_COPIED:
case GIT_DELTA_UNTRACKED: case GIT_DELTA_UNTRACKED:
st = GIT_STATUS_WT_NEW; st = GIT_STATUS_WT_NEW;
...@@ -68,6 +67,9 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status) ...@@ -68,6 +67,9 @@ static unsigned int workdir_delta2status(git_delta_t workdir_status)
case GIT_DELTA_IGNORED: case GIT_DELTA_IGNORED:
st = GIT_STATUS_IGNORED; st = GIT_STATUS_IGNORED;
break; break;
case GIT_DELTA_RENAMED:
st = GIT_STATUS_WT_RENAMED;
break;
case GIT_DELTA_TYPECHANGE: case GIT_DELTA_TYPECHANGE:
st = GIT_STATUS_WT_TYPECHANGE; st = GIT_STATUS_WT_TYPECHANGE;
break; break;
...@@ -136,24 +138,79 @@ static int status_collect( ...@@ -136,24 +138,79 @@ static int status_collect(
return 0; return 0;
} }
git_status_list *git_status_list_alloc(void) GIT_INLINE(int) status_entry_cmp_base(
const void *a,
const void *b,
int (*strcomp)(const char *a, const char *b))
{
const git_status_entry *entry_a = a;
const git_status_entry *entry_b = b;
const git_diff_delta *delta_a, *delta_b;
delta_a = entry_a->index_to_workdir ? entry_a->index_to_workdir :
entry_a->head_to_index;
delta_b = entry_b->index_to_workdir ? entry_b->index_to_workdir :
entry_b->head_to_index;
if (!delta_a && delta_b)
return -1;
if (delta_a && !delta_b)
return 1;
if (!delta_a && !delta_b)
return 0;
return strcomp(delta_a->new_file.path, delta_b->new_file.path);
}
static int status_entry_icmp(const void *a, const void *b)
{
return status_entry_cmp_base(a, b, git__strcasecmp);
}
static int status_entry_cmp(const void *a, const void *b)
{
return status_entry_cmp_base(a, b, git__strcmp);
}
static git_status_list *git_status_list_alloc(git_index *index)
{ {
git_status_list *statuslist = NULL; git_status_list *statuslist = NULL;
int (*entrycmp)(const void *a, const void *b);
entrycmp = index->ignore_case ? status_entry_icmp : status_entry_cmp;
if ((statuslist = git__calloc(1, sizeof(git_status_list))) == NULL || if ((statuslist = git__calloc(1, sizeof(git_status_list))) == NULL ||
git_vector_init(&statuslist->paired, 0, NULL) < 0) git_vector_init(&statuslist->paired, 0, entrycmp) < 0)
return NULL; return NULL;
return statuslist; return statuslist;
} }
static int newfile_cmp(const void *a, const void *b)
{
const git_diff_delta *delta_a = a;
const git_diff_delta *delta_b = b;
return git__strcmp(delta_a->new_file.path, delta_b->new_file.path);
}
static int newfile_casecmp(const void *a, const void *b)
{
const git_diff_delta *delta_a = a;
const git_diff_delta *delta_b = b;
return git__strcasecmp(delta_a->new_file.path, delta_b->new_file.path);
}
int git_status_list_new( int git_status_list_new(
git_status_list **out, git_status_list **out,
git_repository *repo, git_repository *repo,
const git_status_options *opts) const git_status_options *opts)
{ {
git_index *index = NULL;
git_status_list *statuslist = NULL; git_status_list *statuslist = NULL;
git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT; git_diff_options diffopt = GIT_DIFF_OPTIONS_INIT;
git_diff_find_options findopts_i2w = GIT_DIFF_FIND_OPTIONS_INIT;
git_tree *head = NULL; git_tree *head = NULL;
git_status_show_t show = git_status_show_t show =
opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR; opts ? opts->show : GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
...@@ -165,7 +222,8 @@ int git_status_list_new( ...@@ -165,7 +222,8 @@ int git_status_list_new(
GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options"); GITERR_CHECK_VERSION(opts, GIT_STATUS_OPTIONS_VERSION, "git_status_options");
if ((error = git_repository__ensure_not_bare(repo, "status")) < 0) if ((error = git_repository__ensure_not_bare(repo, "status")) < 0 ||
(error = git_repository_index(&index, repo)) < 0)
return error; return error;
/* if there is no HEAD, that's okay - we'll make an empty iterator */ /* if there is no HEAD, that's okay - we'll make an empty iterator */
...@@ -173,7 +231,7 @@ int git_status_list_new( ...@@ -173,7 +231,7 @@ int git_status_list_new(
!(error == GIT_ENOTFOUND || error == GIT_EORPHANEDHEAD)) !(error == GIT_ENOTFOUND || error == GIT_EORPHANEDHEAD))
return error; return error;
statuslist = git_status_list_alloc(); statuslist = git_status_list_alloc(index);
GITERR_CHECK_ALLOC(statuslist); GITERR_CHECK_ALLOC(statuslist);
memcpy(&statuslist->opts, opts, sizeof(git_status_options)); memcpy(&statuslist->opts, opts, sizeof(git_status_options));
...@@ -197,17 +255,23 @@ int git_status_list_new( ...@@ -197,17 +255,23 @@ int git_status_list_new(
if ((opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0) if ((opts->flags & GIT_STATUS_OPT_EXCLUDE_SUBMODULES) != 0)
diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES; diffopt.flags = diffopt.flags | GIT_DIFF_IGNORE_SUBMODULES;
findopts_i2w.flags |= GIT_DIFF_FIND_FOR_UNTRACKED;
if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) { if (show != GIT_STATUS_SHOW_WORKDIR_ONLY) {
error = git_diff_tree_to_index(&statuslist->head2idx, repo, head, NULL, &diffopt); if ((error = git_diff_tree_to_index(&statuslist->head2idx, repo, head, NULL, &diffopt)) < 0)
goto on_error;
if (error < 0) if ((opts->flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 &&
(error = git_diff_find_similar(statuslist->head2idx, NULL)) < 0)
goto on_error; goto on_error;
} }
if (show != GIT_STATUS_SHOW_INDEX_ONLY) { if (show != GIT_STATUS_SHOW_INDEX_ONLY) {
error = git_diff_index_to_workdir(&statuslist->idx2wd, repo, NULL, &diffopt); if ((error = git_diff_index_to_workdir(&statuslist->idx2wd, repo, NULL, &diffopt)) < 0)
goto on_error;
if (error < 0) if ((opts->flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0 &&
(error = git_diff_find_similar(statuslist->idx2wd, &findopts_i2w)) < 0)
goto on_error; goto on_error;
} }
...@@ -219,9 +283,22 @@ int git_status_list_new( ...@@ -219,9 +283,22 @@ int git_status_list_new(
statuslist->head2idx = NULL; statuslist->head2idx = NULL;
} }
if ((error = git_diff__paired_foreach(statuslist->head2idx, statuslist->idx2wd, status_collect, statuslist)) < 0) if ((opts->flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0) {
statuslist->head2idx->deltas._cmp =
(statuslist->head2idx->opts.flags & GIT_DIFF_DELTAS_ARE_ICASE) != 0 ?
newfile_casecmp : newfile_cmp;
git_vector_sort(&statuslist->head2idx->deltas);
}
if ((error = git_diff__paired_foreach(statuslist->head2idx, statuslist->idx2wd,
status_collect, statuslist)) < 0)
goto on_error; goto on_error;
if ((opts->flags & GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX) != 0 ||
(opts->flags & GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR) != 0)
git_vector_sort(&statuslist->paired);
*out = statuslist; *out = statuslist;
goto done; goto done;
...@@ -230,6 +307,7 @@ on_error: ...@@ -230,6 +307,7 @@ on_error:
done: done:
git_tree_free(head); git_tree_free(head);
git_index_free(index);
return error; return error;
} }
......
#include "clar_libgit2.h"
#include "buffer.h"
#include "path.h"
#include "posix.h"
#include "status_helpers.h"
#include "util.h"
#include "status.h"
static git_repository *g_repo = NULL;
void test_status_renames__initialize(void)
{
g_repo = cl_git_sandbox_init("renames");
}
void test_status_renames__cleanup(void)
{
cl_git_sandbox_cleanup();
}
static void rename_file(git_repository *repo, const char *oldname, const char *newname)
{
git_buf oldpath = GIT_BUF_INIT, newpath = GIT_BUF_INIT;
git_buf_joinpath(&oldpath, git_repository_workdir(repo), oldname);
git_buf_joinpath(&newpath, git_repository_workdir(repo), newname);
cl_git_pass(p_rename(oldpath.ptr, newpath.ptr));
git_buf_free(&oldpath);
git_buf_free(&newpath);
}
static void rename_and_edit_file(git_repository *repo, const char *oldname, const char *newname)
{
git_buf oldpath = GIT_BUF_INIT, newpath = GIT_BUF_INIT;
git_buf_joinpath(&oldpath, git_repository_workdir(repo), oldname);
git_buf_joinpath(&newpath, git_repository_workdir(repo), newname);
cl_git_pass(p_rename(oldpath.ptr, newpath.ptr));
cl_git_append2file(newpath.ptr, "Added at the end to keep similarity!");
git_buf_free(&oldpath);
git_buf_free(&newpath);
}
struct status_entry {
git_status_t status;
const char *oldname;
const char *newname;
};
static void test_status(
git_status_list *status_list,
struct status_entry *expected_list,
size_t expected_len)
{
const git_status_entry *actual;
const struct status_entry *expected;
const char *oldname, *newname;
size_t i;
cl_assert(expected_len == git_status_list_entrycount(status_list));
for (i = 0; i < expected_len; i++) {
actual = git_status_byindex(status_list, i);
expected = &expected_list[i];
cl_assert(actual->status == expected->status);
oldname = actual->head_to_index ? actual->head_to_index->old_file.path :
actual->index_to_workdir ? actual->index_to_workdir->old_file.path : NULL;
newname = actual->index_to_workdir ? actual->index_to_workdir->new_file.path :
actual->head_to_index ? actual->head_to_index->new_file.path : NULL;
if (oldname)
cl_assert(git__strcmp(oldname, expected->oldname) == 0);
else
cl_assert(expected->oldname == NULL);
if (newname)
cl_assert(git__strcmp(newname, expected->newname) == 0);
else
cl_assert(expected->newname == NULL);
}
}
void test_status_renames__head2index_one(void)
{
git_index *index;
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "newname.txt" },
};
opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
cl_git_pass(git_repository_index(&index, g_repo));
rename_file(g_repo, "ikeepsix.txt", "newname.txt");
cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt"));
cl_git_pass(git_index_add_bypath(index, "newname.txt"));
cl_git_pass(git_index_write(index));
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 1);
git_status_list_free(statuslist);
git_index_free(index);
}
void test_status_renames__head2index_two(void)
{
git_index *index;
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_INDEX_RENAMED, "sixserving.txt", "aaa.txt" },
{ GIT_STATUS_INDEX_RENAMED, "untimely.txt", "bbb.txt" },
{ GIT_STATUS_INDEX_RENAMED, "songof7cities.txt", "ccc.txt" },
{ GIT_STATUS_INDEX_RENAMED, "ikeepsix.txt", "ddd.txt" },
};
opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
cl_git_pass(git_repository_index(&index, g_repo));
rename_file(g_repo, "ikeepsix.txt", "ddd.txt");
rename_and_edit_file(g_repo, "sixserving.txt", "aaa.txt");
rename_file(g_repo, "songof7cities.txt", "ccc.txt");
rename_and_edit_file(g_repo, "untimely.txt", "bbb.txt");
cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt"));
cl_git_pass(git_index_remove_bypath(index, "sixserving.txt"));
cl_git_pass(git_index_remove_bypath(index, "songof7cities.txt"));
cl_git_pass(git_index_remove_bypath(index, "untimely.txt"));
cl_git_pass(git_index_add_bypath(index, "ddd.txt"));
cl_git_pass(git_index_add_bypath(index, "aaa.txt"));
cl_git_pass(git_index_add_bypath(index, "ccc.txt"));
cl_git_pass(git_index_add_bypath(index, "bbb.txt"));
cl_git_pass(git_index_write(index));
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 4);
git_status_list_free(statuslist);
git_index_free(index);
}
void test_status_renames__index2workdir_one(void)
{
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "newname.txt" },
};
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
rename_file(g_repo, "ikeepsix.txt", "newname.txt");
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 1);
git_status_list_free(statuslist);
}
void test_status_renames__index2workdir_two(void)
{
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_WT_RENAMED, "sixserving.txt", "aaa.txt" },
{ GIT_STATUS_WT_RENAMED, "untimely.txt", "bbb.txt" },
{ GIT_STATUS_WT_RENAMED, "songof7cities.txt", "ccc.txt" },
{ GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "ddd.txt" },
};
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
rename_file(g_repo, "ikeepsix.txt", "ddd.txt");
rename_and_edit_file(g_repo, "sixserving.txt", "aaa.txt");
rename_file(g_repo, "songof7cities.txt", "ccc.txt");
rename_and_edit_file(g_repo, "untimely.txt", "bbb.txt");
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 4);
git_status_list_free(statuslist);
}
void test_status_renames__both_one(void)
{
git_index *index;
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "newname-workdir.txt" },
};
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
cl_git_pass(git_repository_index(&index, g_repo));
rename_file(g_repo, "ikeepsix.txt", "newname-index.txt");
cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt"));
cl_git_pass(git_index_add_bypath(index, "newname-index.txt"));
cl_git_pass(git_index_write(index));
rename_file(g_repo, "newname-index.txt", "newname-workdir.txt");
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 1);
git_status_list_free(statuslist);
git_index_free(index);
}
void test_status_renames__both_two(void)
{
git_index *index;
git_status_list *statuslist;
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
struct status_entry expected[] = {
{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, "ikeepsix.txt", "ikeepsix-both.txt" },
{ GIT_STATUS_INDEX_RENAMED, "sixserving.txt", "sixserving-index.txt" },
{ GIT_STATUS_WT_RENAMED, "songof7cities.txt", "songof7cities-workdir.txt" },
{ GIT_STATUS_INDEX_RENAMED | GIT_STATUS_WT_RENAMED, "untimely.txt", "untimely-both.txt" },
};
opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
opts.flags |= GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
opts.flags |= GIT_STATUS_OPT_RENAMES_INDEX_TO_WORKDIR;
cl_git_pass(git_repository_index(&index, g_repo));
rename_and_edit_file(g_repo, "ikeepsix.txt", "ikeepsix-index.txt");
rename_and_edit_file(g_repo, "sixserving.txt", "sixserving-index.txt");
rename_file(g_repo, "untimely.txt", "untimely-index.txt");
cl_git_pass(git_index_remove_bypath(index, "ikeepsix.txt"));
cl_git_pass(git_index_remove_bypath(index, "sixserving.txt"));
cl_git_pass(git_index_remove_bypath(index, "untimely.txt"));
cl_git_pass(git_index_add_bypath(index, "ikeepsix-index.txt"));
cl_git_pass(git_index_add_bypath(index, "sixserving-index.txt"));
cl_git_pass(git_index_add_bypath(index, "untimely-index.txt"));
cl_git_pass(git_index_write(index));
rename_and_edit_file(g_repo, "ikeepsix-index.txt", "ikeepsix-both.txt");
rename_and_edit_file(g_repo, "songof7cities.txt", "songof7cities-workdir.txt");
rename_file(g_repo, "untimely-index.txt", "untimely-both.txt");
cl_git_pass(git_status_list_new(&statuslist, g_repo, &opts));
test_status(statuslist, expected, 4);
git_status_list_free(statuslist);
git_index_free(index);
}
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