#include "status_subcommand.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
status_subcommand::status_subcommand(const libgit2_object&, CLI::App& app)
{
auto* sub = app.add_subcommand(
"status",
"Show modified files in working directory, staged for your next commit"
);
sub->add_flag("-s,--short", m_options.m_short_flag, "Give the output in the short-format.");
sub->add_flag("--long", m_options.m_long_flag, "Give the output in the long-format. This is the default.");
// sub->add_flag("--porcelain[=]", porcelain, "Give the output in an easy-to-parse format for
// scripts.
// This is similar to the short output, but will remain stable across Git versions and regardless of
// user configuration. See below for details. The version parameter is used to specify the format
// version. This is optional and defaults to the original version v1 format.");
sub->add_flag("-b,--branch", m_options.m_branch_flag, "Show the branch and tracking info even in short-format.");
sub->callback(
[this]()
{
this->run();
}
);
};
namespace
{
const std::string
untracked_header = "Untracked files:\n (use \"git add ...\" to include in what will be committed)\n";
const std::string tobecommited_header = "Changes to be committed:\n (use \"git reset HEAD ...\" to unstage)\n";
const std::string
ignored_header = "Ignored files:\n (use \"git add -f ...\" to include in what will be committed)\n";
const std::string
notstagged_header = "Changes not staged for commit:\n (use \"git add ...\" to update what will be committed)\n";
// TODO: add the following ot notstagged_header after "checkout " is implemented: (use \"git
// checkout -- ...\" to discard changes in working directory)\n";
const std::string unmerged_header = "Unmerged paths:\n (use \"git add ...\" to mark resolution)\n";
const std::string nothingtocommit_message = "no changes added to commit (use \"git add\" and/or \"git commit -a\")";
const std::string treeclean_message = "Nothing to commit, working tree clean";
enum class output_format
{
DEFAULT = 0,
LONG = 1,
SHORT = 2
};
struct print_entry
{
std::string status;
std::string item;
};
struct combined_entry
{
git_status_t index_status = GIT_STATUS_CURRENT;
git_status_t workdir_status = GIT_STATUS_CURRENT;
std::string item;
};
std::string get_print_status(git_status_t status, bool is_long)
{
std::string entry_status;
if (is_long)
{
entry_status = get_status_msg(status).long_mod;
}
else
{
entry_status = get_status_msg(status).short_mod;
}
return entry_status;
}
char short_char_from_status(git_status_t status)
{
switch (status)
{
case GIT_STATUS_INDEX_NEW:
case GIT_STATUS_WT_NEW:
return 'A';
case GIT_STATUS_INDEX_MODIFIED:
case GIT_STATUS_WT_MODIFIED:
return 'M';
case GIT_STATUS_INDEX_DELETED:
case GIT_STATUS_WT_DELETED:
return 'D';
case GIT_STATUS_INDEX_RENAMED:
case GIT_STATUS_WT_RENAMED:
return 'R';
case GIT_STATUS_INDEX_TYPECHANGE:
case GIT_STATUS_WT_TYPECHANGE:
return 'T';
default:
return ' ';
}
}
void update_tracked_dir_set(const char* path, std::set* tracked_dir_set = nullptr)
{
if (tracked_dir_set)
{
const size_t first_slash_idx = std::string_view(path).find('/');
if (std::string::npos != first_slash_idx)
{
auto directory = std::string_view(path).substr(0, first_slash_idx);
tracked_dir_set->insert(std::string(directory));
}
}
}
std::string get_print_item(const char* old_path, const char* new_path)
{
std::string entry_item;
if (old_path && new_path && std::strcmp(old_path, new_path))
{
entry_item = std::string(old_path) + " -> " + std::string(new_path);
}
else
{
entry_item = old_path ? old_path : new_path;
}
return entry_item;
}
std::unordered_map
build_combined_status_map(status_list_wrapper& sl, std::set& tracked_dir_set)
{
std::unordered_map combined;
auto update_status_map =
[&sl, &tracked_dir_set, &combined](const git_status_t(&status_array)[5], bool index)
{
for (git_status_t status : status_array)
{
const auto& list = sl.get_entry_list(status);
for (auto* entry : list)
{
git_diff_delta* dd = index ? entry->head_to_index : entry->index_to_workdir;
const char* old_path = dd->old_file.path;
const char* new_path = dd->new_file.path;
update_tracked_dir_set(old_path, &tracked_dir_set);
std::string item = get_print_item(old_path, new_path);
auto& ce = combined[item];
ce.item = item;
if (index)
{
ce.index_status = status;
}
else
{
ce.workdir_status = status;
}
}
}
};
const git_status_t index_statuses[] = {
GIT_STATUS_INDEX_NEW,
GIT_STATUS_INDEX_MODIFIED,
GIT_STATUS_INDEX_DELETED,
GIT_STATUS_INDEX_RENAMED,
GIT_STATUS_INDEX_TYPECHANGE
};
update_status_map(index_statuses, true);
const git_status_t worktree_statuses[] = {
GIT_STATUS_WT_NEW,
GIT_STATUS_WT_MODIFIED,
GIT_STATUS_WT_DELETED,
GIT_STATUS_WT_TYPECHANGE,
GIT_STATUS_WT_RENAMED
};
update_status_map(worktree_statuses, false);
return combined;
}
void
print_combined_short(const std::unordered_map& entries_map, bool is_coloured)
{
std::vector keys;
keys.reserve(entries_map.size());
for (const auto& kv : entries_map)
{
keys.push_back(kv.first);
}
std::sort(keys.begin(), keys.end());
struct normal_row
{
char idx;
char wt;
std::string item;
};
std::vector normal_rows;
std::vector untracked_items;
std::vector ignored_items;
for (const auto& k : keys)
{
const auto& ce = entries_map.at(k);
// Collect special cases to print last (untracked, ignored) only when not staged.
if ((ce.workdir_status & GIT_STATUS_WT_NEW) && ce.index_status == 0)
{
untracked_items.push_back(ce.item);
continue;
}
if ((ce.workdir_status & GIT_STATUS_IGNORED || ce.index_status & GIT_STATUS_IGNORED)
&& ce.index_status == 0)
{
ignored_items.push_back(ce.item);
continue;
}
// Regular two-column entry (may include index or worktree or both)
char idx = short_char_from_status(ce.index_status);
char wt = short_char_from_status(ce.workdir_status);
normal_rows.push_back({idx, wt, ce.item});
}
for (const auto& r : normal_rows)
{
if (is_coloured)
{
std::cout