mirror of https://gitlab.com/cppit/jucipp
Browse Source
* Git integration, fixes #63 * Fixed a crash when deleting directories, added libgit2 to MSYS2 CI, adjusted colors slightly * Git integration now supports debian stable * Fixed compilation error on MSYS2 * Added git_test * git_test fix * Git integration: now updates correct paths on source save. Also added slight delay to source diff git monitor change signal * git_test fixed * Now monitors .git directory instead. The .git/index file does not always update on for instance: git commit -m ... * Directories cleanup * Fixed git status update on rename refactoring, and some additional cleanup * Added menu items: Go to Next Diff, and Show Diff * Fixed Go to Next Diff and Show Diff keybindings * Minor fixes to git integration * Added: implement method * Minor fixes to Implement Method * Minor fixes to source_diff * source_diff: optimisations added, as well as some minor improvements * Fixed a crash when trying to show diff in a buffer not related to a diff repository * Git integration: MSYS2 support * source_diff: source should now refresh correctly when .git directory has changed * directories.cc: stop updating parent path colors when path including .git directory/file is found * Spellcheck underline no longer shows for for instance '\n' * Made directory view's git status update async * Use boost::filesystem::path in git.* * Optimisation: now stores a cache of git status, which can be slow, that is used when possible * Source view will now grab focus when a selection dialog is shown * Source menu should now be correctly updated * Implement Method: improved * git.cc: minor fixmerge-requests/365/head
27 changed files with 1333 additions and 129 deletions
@ -0,0 +1,284 @@
|
||||
#include "git.h" |
||||
#include <cstring> |
||||
|
||||
std::mutex Git::mutex; |
||||
std::mutex Git::repositories_mutex; |
||||
|
||||
std::string Git::Error::message() noexcept { |
||||
const git_error *last_error = giterr_last(); |
||||
if(last_error==NULL) |
||||
return std::string(); |
||||
else |
||||
return last_error->message; |
||||
} |
||||
|
||||
Git::Repository::Diff::Diff(const boost::filesystem::path &path, git_repository *repository) : repository(repository) { |
||||
blob=std::shared_ptr<git_blob>(nullptr, [](git_blob *blob) { |
||||
if(blob) git_blob_free(blob); |
||||
}); |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
auto spec="HEAD:"+path.generic_string(); |
||||
error.code = git_revparse_single(reinterpret_cast<git_object**>(&blob), repository, spec.c_str()); |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
|
||||
git_diff_init_options(&options, GIT_DIFF_OPTIONS_VERSION); |
||||
options.context_lines=0; |
||||
} |
||||
|
||||
//Based on https://github.com/atom/git-diff/blob/master/lib/git-diff-view.coffee
|
||||
int Git::Repository::Diff::hunk_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, void *payload) noexcept { |
||||
auto lines=static_cast<Lines*>(payload); |
||||
auto start=hunk->new_start-1; |
||||
auto end=hunk->new_start+hunk->new_lines-1; |
||||
if(hunk->old_lines==0 && hunk->new_lines>0) |
||||
lines->added.emplace_back(start, end); |
||||
else if(hunk->new_lines==0 && hunk->old_lines>0) |
||||
lines->removed.emplace_back(start); |
||||
else |
||||
lines->modified.emplace_back(start, end); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
Git::Repository::Diff::Lines Git::Repository::Diff::get_lines(const std::string &buffer) { |
||||
Lines lines; |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
#if LIBGIT2_SOVERSION>=23 |
||||
error.code=git_diff_blob_to_buffer(blob.get(), NULL, buffer.c_str(), buffer.size(), NULL, &options, NULL, NULL, hunk_cb, NULL, &lines); |
||||
#else |
||||
error.code=git_diff_blob_to_buffer(blob.get(), NULL, buffer.c_str(), buffer.size(), NULL, &options, NULL, hunk_cb, NULL, &lines); |
||||
#endif |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
return lines; |
||||
} |
||||
|
||||
int Git::Repository::Diff::line_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) noexcept { |
||||
auto details=static_cast<std::pair<std::string, int> *>(payload); |
||||
auto line_nr=details->second; |
||||
auto start=hunk->new_start-1; |
||||
auto end=hunk->new_start+hunk->new_lines-1; |
||||
if(line_nr==start || (line_nr>=start && line_nr<end)) { |
||||
if(details->first.empty()) |
||||
details->first+=std::string(hunk->header, hunk->header_len); |
||||
details->first+=line->origin+std::string(line->content, line->content_len); |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
std::string Git::Repository::Diff::get_details(const std::string &buffer, int line_nr) { |
||||
std::pair<std::string, int> details; |
||||
details.second=line_nr; |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
#if LIBGIT2_SOVERSION>=23 |
||||
error.code=git_diff_blob_to_buffer(blob.get(), NULL, buffer.c_str(), buffer.size(), NULL, &options, NULL, NULL, NULL, line_cb, &details); |
||||
#else |
||||
error.code=git_diff_blob_to_buffer(blob.get(), NULL, buffer.c_str(), buffer.size(), NULL, &options, NULL, NULL, line_cb, &details); |
||||
#endif |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
return details.first; |
||||
} |
||||
|
||||
Git::Repository::Repository(const boost::filesystem::path &path) { |
||||
git_repository *repository_ptr; |
||||
{ |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
auto path_str=path.generic_string(); |
||||
error.code = git_repository_open_ext(&repository_ptr, path_str.c_str(), 0, NULL); |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
} |
||||
repository=std::unique_ptr<git_repository, std::function<void(git_repository *)> >(repository_ptr, [](git_repository *ptr) { |
||||
git_repository_free(ptr); |
||||
}); |
||||
|
||||
work_path=get_work_path(); |
||||
|
||||
auto git_path_str=boost::filesystem::canonical(get_path()).string(); |
||||
auto git_directory=Glib::wrap(g_file_new_for_path(git_path_str.c_str())); //TODO: report missing constructor in giomm
|
||||
#if GLIB_CHECK_VERSION(2, 44, 0) |
||||
monitor=git_directory->monitor_directory(Gio::FileMonitorFlags::FILE_MONITOR_WATCH_MOVES); |
||||
#else |
||||
monitor=git_directory->monitor_directory(Gio::FileMonitorFlags::FILE_MONITOR_SEND_MOVED); |
||||
#endif |
||||
|
||||
monitor_changed_connection=monitor->signal_changed().connect([this](const Glib::RefPtr<Gio::File> &file, |
||||
const Glib::RefPtr<Gio::File>&, |
||||
Gio::FileMonitorEvent monitor_event) { |
||||
if(monitor_event!=Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { |
||||
this->clear_saved_status(); |
||||
} |
||||
}, false); |
||||
} |
||||
|
||||
Git::Repository::~Repository() { |
||||
monitor_changed_connection.disconnect(); |
||||
} |
||||
|
||||
std::string Git::Repository::status_string(STATUS status) noexcept { |
||||
switch(status) { |
||||
case STATUS::CURRENT: return "current"; |
||||
case STATUS::NEW: return "new"; |
||||
case STATUS::MODIFIED: return "modified"; |
||||
case STATUS::DELETED: return "deleted"; |
||||
case STATUS::RENAMED: return "renamed"; |
||||
case STATUS::TYPECHANGE: return "typechange"; |
||||
case STATUS::UNREADABLE: return "unreadable"; |
||||
case STATUS::IGNORED: return "ignored"; |
||||
case STATUS::CONFLICTED: return "conflicted"; |
||||
default: return ""; |
||||
} |
||||
} |
||||
|
||||
int Git::Repository::status_callback(const char *path, unsigned int status_flags, void *data) noexcept { |
||||
auto callback=static_cast<std::function<void(const char *path, STATUS status)>*>(data); |
||||
|
||||
STATUS status; |
||||
if((status_flags&(GIT_STATUS_INDEX_NEW|GIT_STATUS_WT_NEW))>0) |
||||
status=STATUS::NEW; |
||||
else if((status_flags&(GIT_STATUS_INDEX_MODIFIED|GIT_STATUS_WT_MODIFIED))>0) |
||||
status=STATUS::MODIFIED; |
||||
else if((status_flags&(GIT_STATUS_INDEX_DELETED|GIT_STATUS_WT_DELETED))>0) |
||||
status=STATUS::DELETED; |
||||
else if((status_flags&(GIT_STATUS_INDEX_RENAMED|GIT_STATUS_WT_RENAMED))>0) |
||||
status=STATUS::RENAMED; |
||||
else if((status_flags&(GIT_STATUS_INDEX_TYPECHANGE|GIT_STATUS_WT_TYPECHANGE))>0) |
||||
status=STATUS::TYPECHANGE; |
||||
#if LIBGIT2_SOVERSION>=23 |
||||
else if((status_flags&(GIT_STATUS_WT_UNREADABLE))>0) |
||||
status=STATUS::UNREADABLE; |
||||
#endif |
||||
else if((status_flags&(GIT_STATUS_IGNORED))>0) |
||||
status=STATUS::IGNORED; |
||||
#if LIBGIT2_SOVERSION>=23 |
||||
else if((status_flags&(GIT_STATUS_CONFLICTED))>0) |
||||
status=STATUS::CONFLICTED; |
||||
#endif |
||||
else |
||||
status=STATUS::CURRENT; |
||||
|
||||
if(*callback) |
||||
(*callback)(path, status); |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
Git::Repository::Status Git::Repository::get_status() { |
||||
{ |
||||
std::unique_lock<std::mutex> lock(saved_status_mutex); |
||||
if(has_saved_status) |
||||
return saved_status; |
||||
} |
||||
|
||||
Status status; |
||||
bool first=true; |
||||
std::unique_lock<std::mutex> status_saved_lock(saved_status_mutex, std::defer_lock); |
||||
std::function<void(const char *path, STATUS status)> callback=[this, &status, &first, &status_saved_lock](const char *path_cstr, Git::Repository::STATUS status_enum) { |
||||
if(first) { |
||||
status_saved_lock.lock(); |
||||
first=false; |
||||
} |
||||
boost::filesystem::path rel_path(path_cstr); |
||||
do { |
||||
if(status_enum==Git::Repository::STATUS::MODIFIED) |
||||
status.modified.emplace((work_path/rel_path).generic_string()); |
||||
if(status_enum==Git::Repository::STATUS::NEW) |
||||
status.added.emplace((work_path/rel_path).generic_string()); |
||||
rel_path=rel_path.parent_path(); |
||||
} while(!rel_path.empty()); |
||||
}; |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
error.code = git_status_foreach(repository.get(), Repository::status_callback, &callback); |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
saved_status=status; |
||||
has_saved_status=true; |
||||
if(status_saved_lock) |
||||
status_saved_lock.unlock(); |
||||
return status; |
||||
} |
||||
|
||||
void Git::Repository::clear_saved_status() { |
||||
std::unique_lock<std::mutex> lock(saved_status_mutex); |
||||
saved_status.added.clear(); |
||||
saved_status.modified.clear(); |
||||
has_saved_status=false; |
||||
} |
||||
|
||||
boost::filesystem::path Git::Repository::get_work_path() noexcept { |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
return Git::path(git_repository_workdir(repository.get())); |
||||
} |
||||
|
||||
boost::filesystem::path Git::Repository::get_path() noexcept { |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
return Git::path(git_repository_path(repository.get())); |
||||
} |
||||
|
||||
boost::filesystem::path Git::Repository::root_path(const boost::filesystem::path &path) { |
||||
git_buf root = {0, 0, 0}; |
||||
{ |
||||
Error error; |
||||
std::lock_guard<std::mutex> lock(mutex); |
||||
auto path_str=path.generic_string(); |
||||
error.code = git_repository_discover(&root, path_str.c_str(), 0, NULL); |
||||
if(error) |
||||
throw std::runtime_error(error.message()); |
||||
} |
||||
auto root_path=Git::path(root.ptr, root.size); |
||||
git_buf_free(&root); |
||||
return root_path; |
||||
} |
||||
|
||||
Git::Repository::Diff Git::Repository::get_diff(const boost::filesystem::path &path) { |
||||
return Diff(path, repository.get()); |
||||
} |
||||
|
||||
Git::Git() : repositories(new std::unordered_map<std::string, std::pair<std::unique_ptr<Repository>, size_t> >()) { |
||||
if(!initialized) { |
||||
#if LIBGIT2_SOVERSION>=22 |
||||
git_libgit2_init(); |
||||
#else |
||||
git_threads_init(); |
||||
#endif |
||||
initialized=true; |
||||
} |
||||
} |
||||
|
||||
std::shared_ptr<Git::Repository> Git::get_repository(const boost::filesystem::path &path) { |
||||
std::lock_guard<std::mutex> lock(repositories_mutex); |
||||
auto root_path=std::make_shared<std::string>(Repository::root_path(path).generic_string()); |
||||
auto it=repositories->find(*root_path); |
||||
Repository *repository_ptr; |
||||
if(it!=repositories->end()) { |
||||
it->second.second++; |
||||
repository_ptr=it->second.first.get(); |
||||
} |
||||
else { |
||||
it=repositories->emplace(*root_path, std::pair<std::unique_ptr<Repository>, size_t>(std::unique_ptr<Repository>(new Repository(*root_path)), 1)).first; |
||||
repository_ptr=it->second.first.get(); |
||||
} |
||||
return std::shared_ptr<Repository>(repository_ptr, [this, root_path](Repository *) { |
||||
std::lock_guard<std::mutex> lock(repositories_mutex); |
||||
auto it=repositories->find(*root_path); |
||||
it->second.second--; |
||||
if(it->second.second==0) |
||||
repositories->erase(it); |
||||
}); |
||||
} |
||||
|
||||
boost::filesystem::path Git::path(const char *cpath, size_t cpath_length) noexcept { |
||||
if(cpath_length==static_cast<size_t>(-1)) |
||||
cpath_length=strlen(cpath); |
||||
if(cpath_length>0 && (cpath[cpath_length-1]=='/' || cpath[cpath_length-1]=='\\')) |
||||
return std::string(cpath, cpath_length-1); |
||||
else |
||||
return std::string(cpath, cpath_length); |
||||
} |
||||
@ -0,0 +1,102 @@
|
||||
#ifndef JUCI_GIT_H_ |
||||
#define JUCI_GIT_H_ |
||||
#include <git2.h> |
||||
#include <mutex> |
||||
#include <memory> |
||||
#include <iostream> |
||||
#include <unordered_set> |
||||
#include <unordered_map> |
||||
#include <vector> |
||||
#include <giomm.h> |
||||
#include <boost/filesystem.hpp> |
||||
|
||||
class Git { |
||||
public: |
||||
class Error { |
||||
friend class Git; |
||||
std::string message() noexcept; |
||||
public: |
||||
int code=0; |
||||
Error() {} |
||||
operator bool() noexcept {return code<0;} |
||||
}; |
||||
|
||||
class Repository { |
||||
public: |
||||
class Diff { |
||||
public: |
||||
class Lines { |
||||
public: |
||||
std::vector<std::pair<int, int> > added; |
||||
std::vector<std::pair<int, int> > modified; |
||||
std::vector<int> removed; |
||||
}; |
||||
private: |
||||
friend class Repository; |
||||
Diff(const boost::filesystem::path &path, git_repository *repository); |
||||
git_repository *repository; |
||||
std::shared_ptr<git_blob> blob; |
||||
git_diff_options options; |
||||
static int hunk_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, void *payload) noexcept; |
||||
static int line_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload) noexcept; |
||||
public: |
||||
Diff() : repository(nullptr), blob(nullptr) {} |
||||
Lines get_lines(const std::string &buffer); |
||||
std::string get_details(const std::string &buffer, int line_nr); |
||||
}; |
||||
|
||||
enum class STATUS {CURRENT, NEW, MODIFIED, DELETED, RENAMED, TYPECHANGE, UNREADABLE, IGNORED, CONFLICTED}; |
||||
class Status { |
||||
public: |
||||
std::unordered_set<std::string> added; |
||||
std::unordered_set<std::string> modified; |
||||
}; |
||||
private: |
||||
friend class Git; |
||||
Repository(const boost::filesystem::path &path); |
||||
|
||||
static int status_callback(const char *path, unsigned int status_flags, void *data) noexcept; |
||||
|
||||
std::unique_ptr<git_repository, std::function<void(git_repository *)> > repository; |
||||
|
||||
boost::filesystem::path work_path; |
||||
sigc::connection monitor_changed_connection; |
||||
Status saved_status; |
||||
bool has_saved_status=false; |
||||
std::mutex saved_status_mutex; |
||||
public: |
||||
~Repository(); |
||||
|
||||
static std::string status_string(STATUS status) noexcept; |
||||
|
||||
Status get_status(); |
||||
void clear_saved_status(); |
||||
|
||||
boost::filesystem::path get_work_path() noexcept; |
||||
boost::filesystem::path get_path() noexcept; |
||||
static boost::filesystem::path root_path(const boost::filesystem::path &path); |
||||
|
||||
Diff get_diff(const boost::filesystem::path &path); |
||||
|
||||
Glib::RefPtr<Gio::FileMonitor> monitor; |
||||
}; |
||||
|
||||
private: |
||||
///Mutex for thread safe operations
|
||||
static std::mutex mutex; |
||||
bool initialized=false; |
||||
std::unordered_map<std::string, std::pair<std::unique_ptr<Repository>, size_t> > *repositories; //Freed by OS at program exit
|
||||
static std::mutex repositories_mutex; |
||||
|
||||
Git(); |
||||
|
||||
static boost::filesystem::path path(const char *cpath, size_t cpath_length=static_cast<size_t>(-1)) noexcept; |
||||
|
||||
public: |
||||
static Git &get() noexcept { |
||||
static Git instance; |
||||
return instance; |
||||
} |
||||
std::shared_ptr<Repository> get_repository(const boost::filesystem::path &path); |
||||
}; |
||||
#endif //JUCI_GIT_H_
|
||||
@ -0,0 +1,369 @@
|
||||
#include "source_diff.h" |
||||
#include "config.h" |
||||
#include "terminal.h" |
||||
#include <boost/version.hpp> |
||||
|
||||
namespace sigc { |
||||
#ifndef SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE |
||||
template <typename Functor> |
||||
struct functor_trait<Functor, false> { |
||||
typedef decltype (::sigc::mem_fun(std::declval<Functor&>(), |
||||
&Functor::operator())) _intermediate; |
||||
typedef typename _intermediate::result_type result_type; |
||||
typedef Functor functor_type; |
||||
}; |
||||
#else |
||||
SIGC_FUNCTORS_DEDUCE_RESULT_TYPE_WITH_DECLTYPE |
||||
#endif |
||||
} |
||||
|
||||
Source::DiffView::Renderer::Renderer() : Gsv::GutterRenderer() { |
||||
set_padding(4, 0); |
||||
} |
||||
|
||||
void Source::DiffView::Renderer::draw_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, const Gdk::Rectangle &background_area, |
||||
const Gdk::Rectangle &cell_area, Gtk::TextIter &start, Gtk::TextIter &end, |
||||
Gsv::GutterRendererState p6) { |
||||
if(start.has_tag(tag_added) || end.has_tag(tag_added)) { |
||||
cr->set_source_rgba(0.0, 1.0, 0.0, 0.5); |
||||
cr->rectangle(cell_area.get_x(), cell_area.get_y(), 4, cell_area.get_height()); |
||||
cr->fill(); |
||||
} |
||||
else if(start.has_tag(tag_modified) || end.has_tag(tag_modified)) { |
||||
cr->set_source_rgba(0.9, 0.9, 0.0, 0.75); |
||||
cr->rectangle(cell_area.get_x(), cell_area.get_y(), 4, cell_area.get_height()); |
||||
cr->fill(); |
||||
} |
||||
if(start.has_tag(tag_removed_below) || end.has_tag(tag_removed_below)) { |
||||
cr->set_source_rgba(0.75, 0.0, 0.0, 0.5); |
||||
cr->rectangle(cell_area.get_x()-4, cell_area.get_y()+cell_area.get_height()-2, 8, 2); |
||||
cr->fill(); |
||||
} |
||||
if(start.has_tag(tag_removed_above) || end.has_tag(tag_removed_above)) { |
||||
cr->set_source_rgba(0.75, 0.0, 0.0, 0.5); |
||||
cr->rectangle(cell_area.get_x()-4, cell_area.get_y(), 8, 2); |
||||
cr->fill(); |
||||
} |
||||
} |
||||
|
||||
Source::DiffView::DiffView(const boost::filesystem::path &file_path) : Gsv::View(), file_path(file_path), renderer(new Renderer()) { |
||||
renderer->tag_added=get_buffer()->create_tag("git_added"); |
||||
renderer->tag_modified=get_buffer()->create_tag("git_modified"); |
||||
renderer->tag_removed=get_buffer()->create_tag("git_removed"); |
||||
renderer->tag_removed_below=get_buffer()->create_tag(); |
||||
renderer->tag_removed_above=get_buffer()->create_tag(); |
||||
|
||||
configure(); |
||||
} |
||||
|
||||
Source::DiffView::~DiffView() { |
||||
dispatcher.disconnect(); |
||||
if(repository) { |
||||
get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->remove(renderer.get()); |
||||
buffer_insert_connection.disconnect(); |
||||
buffer_erase_connection.disconnect(); |
||||
monitor_changed_connection.disconnect(); |
||||
delayed_buffer_changed_connection.disconnect(); |
||||
delayed_monitor_changed_connection.disconnect(); |
||||
|
||||
parse_stop=true; |
||||
if(parse_thread.joinable()) |
||||
parse_thread.join(); |
||||
} |
||||
} |
||||
|
||||
void Source::DiffView::configure() { |
||||
if(Config::get().source.show_git_diff) { |
||||
if(repository) |
||||
return; |
||||
} |
||||
else if(repository) { |
||||
get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->remove(renderer.get()); |
||||
buffer_insert_connection.disconnect(); |
||||
buffer_erase_connection.disconnect(); |
||||
monitor_changed_connection.disconnect(); |
||||
delayed_buffer_changed_connection.disconnect(); |
||||
delayed_monitor_changed_connection.disconnect(); |
||||
|
||||
parse_stop=true; |
||||
if(parse_thread.joinable()) |
||||
parse_thread.join(); |
||||
repository=nullptr; |
||||
diff=nullptr; |
||||
|
||||
return; |
||||
} |
||||
else |
||||
return; |
||||
|
||||
try { |
||||
repository=Git::get().get_repository(this->file_path.parent_path()); |
||||
} |
||||
catch(const std::exception &) { |
||||
return; |
||||
} |
||||
|
||||
get_gutter(Gtk::TextWindowType::TEXT_WINDOW_LEFT)->insert(renderer.get(), -40); |
||||
parse_state=ParseState::STARTING; |
||||
parse_stop=false; |
||||
monitor_changed=false; |
||||
|
||||
buffer_insert_connection=get_buffer()->signal_insert().connect([this](const Gtk::TextBuffer::iterator &iter ,const Glib::ustring &text, int) { |
||||
//Do not perform git diff if no newline is added and line is already marked as added
|
||||
if(!iter.starts_line() && iter.has_tag(renderer->tag_added)) { |
||||
bool newline=false; |
||||
for(auto &c: text.raw()) { |
||||
if(c=='\n') { |
||||
newline=true; |
||||
break; |
||||
} |
||||
} |
||||
if(!newline) |
||||
return; |
||||
} |
||||
//Remove tag_removed_above/below if newline is inserted
|
||||
else if(!text.empty() && text[0]=='\n' && iter.has_tag(renderer->tag_removed)) { |
||||
auto start_iter=get_buffer()->get_iter_at_line(iter.get_line()); |
||||
auto end_iter=get_iter_at_line_end(iter.get_line()); |
||||
end_iter.forward_char(); |
||||
get_buffer()->remove_tag(renderer->tag_removed_above, start_iter, end_iter); |
||||
get_buffer()->remove_tag(renderer->tag_removed_below, start_iter, end_iter); |
||||
} |
||||
parse_state=ParseState::IDLE; |
||||
delayed_buffer_changed_connection.disconnect(); |
||||
delayed_buffer_changed_connection=Glib::signal_timeout().connect([this]() { |
||||
parse_state=ParseState::STARTING; |
||||
return false; |
||||
}, 250); |
||||
}, false); |
||||
|
||||
buffer_erase_connection=get_buffer()->signal_erase().connect([this](const Gtk::TextBuffer::iterator &start_iter, const Gtk::TextBuffer::iterator &end_iter) { |
||||
//Do not perform git diff if start_iter and end_iter is at the same line in addition to the line is tagged added
|
||||
if(start_iter.get_line()==end_iter.get_line() && start_iter.has_tag(renderer->tag_added)) |
||||
return; |
||||
|
||||
parse_state=ParseState::IDLE; |
||||
delayed_buffer_changed_connection.disconnect(); |
||||
delayed_buffer_changed_connection=Glib::signal_timeout().connect([this]() { |
||||
parse_state=ParseState::STARTING; |
||||
return false; |
||||
}, 250); |
||||
}, false); |
||||
|
||||
monitor_changed_connection=repository->monitor->signal_changed().connect([this](const Glib::RefPtr<Gio::File> &file, |
||||
const Glib::RefPtr<Gio::File>&, |
||||
Gio::FileMonitorEvent monitor_event) { |
||||
if(monitor_event!=Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { |
||||
delayed_monitor_changed_connection.disconnect(); |
||||
delayed_monitor_changed_connection=Glib::signal_timeout().connect([this]() { |
||||
monitor_changed=true; |
||||
parse_state=ParseState::STARTING; |
||||
std::unique_lock<std::mutex> lock(parse_mutex); |
||||
diff=nullptr; |
||||
return false; |
||||
}, 500); |
||||
} |
||||
}); |
||||
|
||||
parse_thread=std::thread([this]() { |
||||
try { |
||||
diff=get_diff(); |
||||
} |
||||
catch(const std::exception &) {} |
||||
|
||||
try { |
||||
while(true) { |
||||
while(!parse_stop && parse_state!=ParseState::STARTING && parse_state!=ParseState::PROCESSING) |
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10)); |
||||
if(parse_stop) |
||||
break; |
||||
std::unique_lock<std::mutex> parse_lock(parse_mutex, std::defer_lock); |
||||
auto expected=ParseState::STARTING; |
||||
if(parse_state.compare_exchange_strong(expected, ParseState::PREPROCESSING)) { |
||||
dispatcher.post([this] { |
||||
auto expected=ParseState::PREPROCESSING; |
||||
std::unique_lock<std::mutex> parse_lock(parse_mutex, std::defer_lock); |
||||
if(parse_lock.try_lock()) { |
||||
if(parse_state.compare_exchange_strong(expected, ParseState::PROCESSING)) |
||||
parse_buffer=get_buffer()->get_text(); |
||||
parse_lock.unlock(); |
||||
} |
||||
else |
||||
parse_state.compare_exchange_strong(expected, ParseState::STARTING); |
||||
}); |
||||
} |
||||
else if (parse_state==ParseState::PROCESSING && parse_lock.try_lock()) { |
||||
bool expected_monitor_changed=true; |
||||
if(monitor_changed.compare_exchange_strong(expected_monitor_changed, false)) { |
||||
try { |
||||
diff=get_diff(); |
||||
} |
||||
catch(const std::exception &) { |
||||
dispatcher.post([this] { |
||||
get_buffer()->remove_tag(renderer->tag_added, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_modified, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_below, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_above, get_buffer()->begin(), get_buffer()->end()); |
||||
renderer->queue_draw(); |
||||
}); |
||||
} |
||||
} |
||||
if(diff) |
||||
lines=diff->get_lines(parse_buffer.raw()); |
||||
else { |
||||
lines.added.clear(); |
||||
lines.modified.clear(); |
||||
lines.removed.clear(); |
||||
} |
||||
auto expected=ParseState::PROCESSING; |
||||
if(parse_state.compare_exchange_strong(expected, ParseState::POSTPROCESSING)) { |
||||
parse_lock.unlock(); |
||||
dispatcher.post([this] { |
||||
std::unique_lock<std::mutex> parse_lock(parse_mutex, std::defer_lock); |
||||
if(parse_lock.try_lock()) { |
||||
auto expected=ParseState::POSTPROCESSING; |
||||
if(parse_state.compare_exchange_strong(expected, ParseState::IDLE)) |
||||
update_lines(); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
catch(const std::exception &e) { |
||||
auto e_what=std::make_shared<std::string>(e.what()); |
||||
dispatcher.post([this, e_what] { |
||||
get_buffer()->remove_tag(renderer->tag_added, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_modified, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_below, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_above, get_buffer()->begin(), get_buffer()->end()); |
||||
renderer->queue_draw(); |
||||
Terminal::get().print("Error (git): "+*e_what+'\n', true); |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
Gtk::TextIter Source::DiffView::get_iter_at_line_end(int line_nr) { |
||||
if(line_nr>=get_buffer()->get_line_count()) |
||||
return get_buffer()->end(); |
||||
else if(line_nr+1<get_buffer()->get_line_count()) { |
||||
auto iter=get_buffer()->get_iter_at_line(line_nr+1); |
||||
iter.backward_char(); |
||||
return iter; |
||||
} |
||||
else { |
||||
auto iter=get_buffer()->get_iter_at_line(line_nr); |
||||
while(!iter.ends_line() && iter.forward_char()) {} |
||||
return iter; |
||||
} |
||||
} |
||||
|
||||
void Source::DiffView::git_goto_next_diff() { |
||||
auto iter=get_buffer()->get_insert()->get_iter(); |
||||
auto insert_iter=iter; |
||||
bool wrapped=false; |
||||
iter.forward_char(); |
||||
while(!wrapped || iter<insert_iter) { |
||||
auto toggled_tags=iter.get_toggled_tags(); |
||||
for(auto &toggled_tag: toggled_tags) { |
||||
if(toggled_tag->property_name()=="git_added" || |
||||
toggled_tag->property_name()=="git_modified" || |
||||
toggled_tag->property_name()=="git_removed") { |
||||
get_buffer()->place_cursor(iter); |
||||
scroll_to(get_buffer()->get_insert(), 0.0, 1.0, 0.5); |
||||
return; |
||||
} |
||||
} |
||||
iter.forward_char(); |
||||
if(!wrapped && iter==get_buffer()->end()) { |
||||
iter=get_buffer()->begin(); |
||||
wrapped=true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
std::string Source::DiffView::git_get_diff_details() { |
||||
if(!diff) |
||||
return std::string(); |
||||
auto line_nr=get_buffer()->get_insert()->get_iter().get_line(); |
||||
auto iter=get_buffer()->get_iter_at_line(line_nr); |
||||
if(iter.has_tag(renderer->tag_removed_above)) |
||||
--line_nr; |
||||
std::unique_lock<std::mutex> lock(parse_mutex); |
||||
parse_buffer=get_buffer()->get_text(); |
||||
return diff->get_details(parse_buffer.raw(), line_nr); |
||||
} |
||||
|
||||
///Return repository diff instance. Throws exception on error
|
||||
std::unique_ptr<Git::Repository::Diff> Source::DiffView::get_diff() { |
||||
auto work_path=boost::filesystem::canonical(repository->get_work_path()); |
||||
boost::filesystem::path relative_path; |
||||
{ |
||||
std::unique_lock<std::mutex> lock(file_path_mutex); |
||||
#if BOOST_VERSION>=106000 |
||||
relative_path=boost::filesystem::relative(file_path, work_path); |
||||
#else |
||||
if(std::distance(file_path.begin(), file_path.end())<std::distance(work_path.begin(), work_path.end())) |
||||
throw std::runtime_error("not a relative path"); |
||||
|
||||
auto work_path_it=work_path.begin(); |
||||
auto file_path_it=file_path.begin(); |
||||
while(file_path_it!=file_path.end() && work_path_it!=work_path.end()) { |
||||
if(*file_path_it!=*work_path_it) |
||||
throw std::runtime_error("not a relative path"); |
||||
++file_path_it; |
||||
++work_path_it; |
||||
} |
||||
for(;file_path_it!=file_path.end();++file_path_it) |
||||
relative_path/=*file_path_it; |
||||
#endif |
||||
} |
||||
return std::unique_ptr<Git::Repository::Diff>(new Git::Repository::Diff(repository->get_diff(relative_path))); |
||||
} |
||||
|
||||
void Source::DiffView::update_lines() { |
||||
get_buffer()->remove_tag(renderer->tag_added, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_modified, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_below, get_buffer()->begin(), get_buffer()->end()); |
||||
get_buffer()->remove_tag(renderer->tag_removed_above, get_buffer()->begin(), get_buffer()->end()); |
||||
|
||||
for(auto &added: lines.added) { |
||||
auto start_iter=get_buffer()->get_iter_at_line(added.first); |
||||
auto end_iter=get_iter_at_line_end(added.second-1); |
||||
end_iter.forward_char(); |
||||
get_buffer()->apply_tag(renderer->tag_added, start_iter, end_iter); |
||||
} |
||||
for(auto &modified: lines.modified) { |
||||
auto start_iter=get_buffer()->get_iter_at_line(modified.first); |
||||
auto end_iter=get_iter_at_line_end(modified.second-1); |
||||
end_iter.forward_char(); |
||||
get_buffer()->apply_tag(renderer->tag_modified, start_iter, end_iter); |
||||
} |
||||
for(auto &line_nr: lines.removed) { |
||||
Gtk::TextIter removed_start, removed_end; |
||||
if(line_nr>=0) { |
||||
auto start_iter=get_buffer()->get_iter_at_line(line_nr); |
||||
removed_start=start_iter; |
||||
auto end_iter=get_iter_at_line_end(line_nr); |
||||
end_iter.forward_char(); |
||||
removed_end=end_iter; |
||||
get_buffer()->apply_tag(renderer->tag_removed_below, start_iter, end_iter); |
||||
} |
||||
if(line_nr+1<get_buffer()->get_line_count()) { |
||||
auto start_iter=get_buffer()->get_iter_at_line(line_nr+1); |
||||
if(line_nr<0) |
||||
removed_start=start_iter; |
||||
auto end_iter=get_iter_at_line_end(line_nr+1); |
||||
end_iter.forward_char(); |
||||
removed_end=end_iter; |
||||
get_buffer()->apply_tag(renderer->tag_removed_above, start_iter, end_iter); |
||||
} |
||||
get_buffer()->apply_tag(renderer->tag_removed, removed_start, removed_end); |
||||
} |
||||
|
||||
renderer->queue_draw(); |
||||
} |
||||
@ -0,0 +1,71 @@
|
||||
#ifndef JUCI_SOURCE_DIFF_H_ |
||||
#define JUCI_SOURCE_DIFF_H_ |
||||
#include <gtksourceviewmm.h> |
||||
#include <boost/filesystem.hpp> |
||||
#include "dispatcher.h" |
||||
#include <set> |
||||
#include <map> |
||||
#include <thread> |
||||
#include <atomic> |
||||
#include <mutex> |
||||
#include "git.h" |
||||
|
||||
namespace Source { |
||||
class DiffView : virtual public Gsv::View { |
||||
enum class ParseState {IDLE, STARTING, PREPROCESSING, PROCESSING, POSTPROCESSING}; |
||||
|
||||
class Renderer : public Gsv::GutterRenderer { |
||||
public: |
||||
Renderer(); |
||||
|
||||
Glib::RefPtr<Gtk::TextTag> tag_added; |
||||
Glib::RefPtr<Gtk::TextTag> tag_modified; |
||||
Glib::RefPtr<Gtk::TextTag> tag_removed; |
||||
Glib::RefPtr<Gtk::TextTag> tag_removed_below; |
||||
Glib::RefPtr<Gtk::TextTag> tag_removed_above; |
||||
|
||||
protected: |
||||
void draw_vfunc(const Cairo::RefPtr<Cairo::Context> &cr, const Gdk::Rectangle &background_area, |
||||
const Gdk::Rectangle &cell_area, Gtk::TextIter &start, Gtk::TextIter &end, |
||||
Gsv::GutterRendererState p6) override; |
||||
}; |
||||
public: |
||||
DiffView(const boost::filesystem::path &file_path); |
||||
~DiffView(); |
||||
|
||||
virtual void configure(); |
||||
|
||||
Gtk::TextIter get_iter_at_line_end(int line_nr); |
||||
|
||||
void git_goto_next_diff(); |
||||
std::string git_get_diff_details(); |
||||
|
||||
boost::filesystem::path file_path; |
||||
///Only needed when using file_path in a thread, or when changing file_path
|
||||
std::mutex file_path_mutex; |
||||
private: |
||||
std::unique_ptr<Renderer> renderer; |
||||
Dispatcher dispatcher; |
||||
|
||||
std::shared_ptr<Git::Repository> repository; |
||||
std::unique_ptr<Git::Repository::Diff> diff; |
||||
std::unique_ptr<Git::Repository::Diff> get_diff(); |
||||
|
||||
std::thread parse_thread; |
||||
std::atomic<ParseState> parse_state; |
||||
std::mutex parse_mutex; |
||||
std::atomic<bool> parse_stop; |
||||
Glib::ustring parse_buffer; |
||||
sigc::connection buffer_insert_connection; |
||||
sigc::connection buffer_erase_connection; |
||||
sigc::connection monitor_changed_connection; |
||||
sigc::connection delayed_buffer_changed_connection; |
||||
sigc::connection delayed_monitor_changed_connection; |
||||
std::atomic<bool> monitor_changed; |
||||
|
||||
Git::Repository::Diff::Lines lines; |
||||
void update_lines(); |
||||
}; |
||||
} |
||||
|
||||
#endif //JUCI_SOURCE_DIFF_H_
|
||||
@ -0,0 +1,41 @@
|
||||
#include <glib.h> |
||||
#include <gtkmm.h> |
||||
#include "git.h" |
||||
#include <boost/filesystem.hpp> |
||||
|
||||
int main() { |
||||
auto app=Gtk::Application::create(); |
||||
|
||||
auto tests_path=boost::filesystem::canonical(JUCI_TESTS_PATH); |
||||
auto jucipp_path=tests_path.parent_path(); |
||||
auto git_path=jucipp_path/".git"; |
||||
|
||||
try { |
||||
auto repository=Git::get().get_repository(tests_path); |
||||
|
||||
g_assert(repository->get_path()==git_path); |
||||
g_assert(repository->get_work_path()==jucipp_path); |
||||
|
||||
auto status=repository->get_status(); |
||||
|
||||
auto diff=repository->get_diff((boost::filesystem::path("tests")/"git_test.cc")); |
||||
auto lines=diff.get_lines("#include added\n#include <glib.h>\n#include modified\n#include \"git.h\"\n"); |
||||
g_assert_cmpuint(lines.added.size(), ==, 1); |
||||
g_assert_cmpuint(lines.modified.size(), ==, 1); |
||||
g_assert_cmpuint(lines.removed.size(), ==, 1); |
||||
} |
||||
catch(const std::exception &e) { |
||||
std::cerr << e.what() << std::endl; |
||||
return 1; |
||||
} |
||||
|
||||
try { |
||||
g_assert(Git::Repository::root_path(tests_path)==git_path); |
||||
} |
||||
catch(const std::exception &e) { |
||||
std::cerr << e.what() << std::endl; |
||||
return 1; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
@ -0,0 +1,11 @@
|
||||
#include "directories.h" |
||||
|
||||
Directories::Directories() {} |
||||
|
||||
Directories::~Directories() {} |
||||
|
||||
void Directories::on_save_file(boost::filesystem::path file_path) {} |
||||
|
||||
bool Directories::on_button_press_event(GdkEventButton *event) { |
||||
return false; |
||||
}; |
||||
Loading…
Reference in new issue