Index: plugins/Makefile.am =================================================================== --- plugins/Makefile.am (revision 2014) +++ plugins/Makefile.am (working copy) @@ -11,6 +11,7 @@ htmlchars_la_LDFLAGS = -module -avoid-version export_la_LDFLAGS = -module -avoid-version svndiff_la_LDFLAGS = -module -avoid-version +vcdiff_la_LDFLAGS = -module -avoid-version filebrowser_la_LDFLAGS = -module -avoid-version if PLUGINS @@ -21,6 +22,7 @@ htmlchars.la \ export.la \ svndiff.la \ + vcdiff.la \ filebrowser.la # Plugins not to be installed @@ -32,6 +34,7 @@ htmlchars_la_SOURCES = htmlchars.c export_la_SOURCES = export.c svndiff_la_SOURCES = svndiff.c +vcdiff_la_SOURCES = vcdiff.c filebrowser_la_SOURCES = filebrowser.c demoplugin_la_LIBADD = $(GTK_LIBS) @@ -39,6 +42,7 @@ htmlchars_la_LIBADD = $(GTK_LIBS) export_la_LIBADD = $(GTK_LIBS) svndiff_la_LIBADD = $(GTK_LIBS) +vcdiff_la_LIBADD = $(GTK_LIBS) filebrowser_la_LIBADD = $(GTK_LIBS) endif # PLUGINS Index: plugins/vcdiff.c =================================================================== --- plugins/vcdiff.c (revision 0) +++ plugins/vcdiff.c (revision 0) @@ -0,0 +1,549 @@ +/* + * vcdiff.c - this file is part of Geany, a fast and lightweight IDE + * + * Copyright 2007 Frank Lanitz + * Copyright 2007 Enrico Tröger + * Copyright 2007 Nick Treleaven + * Copyright 2007 Yura Siamashka + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/* VCdiff plugin */ +/* This small plugin uses svn/git to generate a diff against the current + * version inside vc.*/ + +#include "geany.h" +#include "support.h" +#include "plugindata.h" +#include "document.h" +#include "filetypes.h" +#include "utils.h" +#include "project.h" +#include "pluginmacros.h" + +PluginFields *plugin_fields; +GeanyData *geany_data; + + +VERSION_CHECK(27) + +PLUGIN_INFO(_("VCdiff"), _("Plugin to create a patch of a file against vc"), VERSION) + +struct VC_RECORD +{ + void** commands; + void** envs; + gboolean (*predicate)(const gchar* filename); +}; + +#define VC_COMMAND_DIFF_FILE 0 +#define VC_COMMAND_DIFF_DIR 1 +#define VC_COMMAND_DIFF_PROJECT 2 + +static const gchar DIRNAME[] = "*DIRNAME*"; +static const gchar FILENAME[] = "*FILENAME*"; +static const gchar BASE_FILENAME[] = "*BASE_FILENAME*"; + +static const gchar* SVN_CMD_DIFF_FILE[] = {"svn", "diff", "--non-interactive", FILENAME, NULL}; +static const gchar* SVN_CMD_DIFF_DIR[] = {"svn", "diff", "--non-interactive", DIRNAME, NULL}; +static const gchar* SVN_CMD_DIFF_PROJECT[] = {"svn", "diff", "--non-interactive", DIRNAME, NULL}; + +static const gchar* SVN_ENV_DIFF_FILE[] = {NULL}; +static const gchar* SVN_ENV_DIFF_DIR[] = {NULL}; +static const gchar* SVN_ENV_DIFF_PROJECT[] = {NULL}; + + +static void* SVN_COMMANDS[] = { SVN_CMD_DIFF_FILE, SVN_CMD_DIFF_DIR, SVN_CMD_DIFF_PROJECT }; +static void* SVN_ENV[] = { SVN_ENV_DIFF_FILE, SVN_ENV_DIFF_DIR, SVN_ENV_DIFF_PROJECT }; + +static gboolean is_svn(const gchar* filename) +{ + gboolean ret; + gchar *base; + gchar *svndir; + gchar *svnpath; + + svnpath = g_find_program_in_path("svn"); + if (!svnpath) + return FALSE; + g_free(svnpath); + + if (g_file_test(filename, G_FILE_TEST_IS_DIR)) + base = g_strdup(filename); + else + base = g_path_get_dirname(filename); + svndir = g_build_path("/", base, ".svn", NULL); + + ret = g_file_test(svndir, G_FILE_TEST_IS_DIR); + + g_free(base); + g_free(svndir); + + return ret; +} + +static const gchar* CVS_CMD_DIFF_FILE[] = {"cvs", "diff", "-u", BASE_FILENAME, NULL}; +static const gchar* CVS_CMD_DIFF_DIR[] = {"cvs", "diff", "-u",NULL}; +static const gchar* CVS_CMD_DIFF_PROJECT[] = {"cvs", "diff", "-u", NULL}; + +static const gchar* CVS_ENV_DIFF_FILE[] = {NULL}; +static const gchar* CVS_ENV_DIFF_DIR[] = {NULL}; +static const gchar* CVS_ENV_DIFF_PROJECT[] = {NULL}; + + +static void* CVS_COMMANDS[] = { CVS_CMD_DIFF_FILE, CVS_CMD_DIFF_DIR, CVS_CMD_DIFF_PROJECT }; +static void* CVS_ENV[] = { CVS_ENV_DIFF_FILE, CVS_ENV_DIFF_DIR, CVS_ENV_DIFF_PROJECT }; + +static gboolean is_cvs(const gchar* filename) +{ + gboolean ret; + gchar *base; + gchar *cvsdir; + gchar *cvspath; + + cvspath = g_find_program_in_path("cvs"); + if (!cvspath) + return FALSE; + g_free(cvspath); + + if (g_file_test(filename, G_FILE_TEST_IS_DIR)) + base = g_strdup(filename); + else + base = g_path_get_dirname(filename); + cvsdir = g_build_path("/", base, "CVS", NULL); + + ret = g_file_test(cvsdir, G_FILE_TEST_IS_DIR); + + g_free(base); + g_free(cvsdir); + + return ret; +} + + +static const gchar* GIT_CMD_DIFF_FILE[] = {"git", "diff", BASE_FILENAME, NULL}; +static const gchar* GIT_CMD_DIFF_DIR[] = {"git", "diff", NULL}; +static const gchar* GIT_CMD_DIFF_PROJECT[] = {"git", "diff", NULL}; + +static const gchar* GIT_ENV_DIFF_FILE[] = {"PAGER=cat", NULL}; +static const gchar* GIT_ENV_DIFF_DIR[] = {"PAGER=cat", NULL}; +static const gchar* GIT_ENV_DIFF_PROJECT[] = {"PAGER=cat", NULL}; + + +static void* GIT_COMMANDS[] = { GIT_CMD_DIFF_FILE, GIT_CMD_DIFF_DIR, GIT_CMD_DIFF_PROJECT }; +static void* GIT_ENV[] = { GIT_ENV_DIFF_FILE, GIT_ENV_DIFF_DIR, GIT_ENV_DIFF_PROJECT }; + +static gboolean is_git(const gchar* filename) +{ + gboolean ret = FALSE; + gchar *base; + gchar *gitdir; + gchar *gitpath; + gchar *tmp; + gchar *base_prev = g_strdup(":"); + + gitpath = g_find_program_in_path("git"); + if (!gitpath) + return FALSE; + g_free(gitpath); + + if (g_file_test(filename, G_FILE_TEST_IS_DIR)) + base = g_strdup(filename); + else + base = g_path_get_dirname(filename); + + while(strcmp(base, base_prev) != 0) + { + gitdir = g_build_path("/", base, ".git", NULL); + ret = g_file_test(gitdir, G_FILE_TEST_IS_DIR); + g_free(gitdir); + if (ret) + break; + g_free(base_prev); + base_prev = base; + base = g_path_get_dirname(base); + } + + g_free(base); + g_free(base_prev); + return ret; +} + +static struct VC_RECORD VC[] = { + {SVN_COMMANDS, SVN_ENV, is_svn}, + {CVS_COMMANDS, CVS_ENV, is_cvs}, + {GIT_COMMANDS, GIT_ENV, is_git}, + {NULL, NULL, NULL} +}; + +static void* find_cmd_env(gint cmd_type, gboolean cmd, const gchar* filename) +{ + int i = 0; + while(1) + { + if (!VC[i].predicate) + return NULL; + if (VC[i].predicate(filename)) + if (cmd) + return VC[i].commands[cmd_type]; + else + return VC[i].envs[cmd_type]; + i++; + } +} + +static void* get_cmd_env(gint cmd_type, gboolean cmd, const gchar* filename) +{ + int i; + gint len = 0; + gchar** argv; + gchar** ret; + gchar* dir; + gchar* base_filename; + + argv = find_cmd_env(cmd_type, cmd, filename); + if (!argv) + return NULL; + + if (g_file_test(filename, G_FILE_TEST_IS_DIR)) + { + dir = g_strdup(filename); + } + else + { + dir = g_path_get_dirname(filename); + } + base_filename = g_path_get_basename(filename); + + while(1) + { + if (argv[len] == NULL) + break; + len++; + } + ret = g_malloc(sizeof(gchar*) * (len+1)); + memset(ret, 0, sizeof(gchar*) * (len+1)); + for (i = 0; i < len; i++) + { + if (argv[i] == DIRNAME) + { + ret[i] = g_strdup(dir); + } + else if (argv[i] == FILENAME) + { + ret[i] = g_strdup(filename); + } + else if (argv[i] == BASE_FILENAME) + { + ret[i] = g_strdup(base_filename); + } + else + ret[i] = g_strdup(argv[i]); + } + g_free(dir); + g_free(base_filename); + return ret; +} + +static int find_by_filename(const gchar* filename) +{ + gint i; + for (i = 0; i < doc_array->len; i++) + { + if ( DOC_IDX_VALID(i) && doc_list[i].file_name && + strcmp(doc_list[i].file_name, filename) == 0) + return i; + } + return -1; +} + +/* name_prefix should be in UTF-8, and can have a path. */ +static void show_output(const gchar *std_output, const gchar *name_prefix, + const gchar *force_encoding) +{ + gchar *text, *detect_enc = NULL; + gint idx, page; + GtkNotebook *book; + gchar *filename; + + filename = g_path_get_basename(name_prefix); + setptr(filename, g_strconcat(filename, ".vc.diff", NULL)); + + // need to convert input text from the encoding of the original file into + // UTF-8 because internally Geany always needs UTF-8 + if (force_encoding) + { + text = geany_data->encoding->convert_to_utf8_from_charset( + std_output, -1, force_encoding, TRUE); + } + else + { + text = geany_data->encoding->convert_to_utf8(std_output, -1, &detect_enc); + } + if (text) + { + idx = find_by_filename(filename); + if ( idx == -1) + { + idx = geany_data->document->new_file(filename, + geany_data->filetypes[GEANY_FILETYPES_DIFF], text); + } + else + { + geany_data->sci->set_text(doc_list[idx].sci, text); + book = GTK_NOTEBOOK(app->notebook); + page = gtk_notebook_page_num(book, GTK_WIDGET(doc_list[idx].sci)); + gtk_notebook_set_current_page(book, page); + doc_list[idx].changed = FALSE; + documents->set_text_changed(idx); + } + + geany_data->document->set_encoding(idx, + force_encoding ? force_encoding : detect_enc); + } + else + { + ui->set_statusbar(FALSE, _("Could not parse the output of the diff")); + } + g_free(text); + g_free(detect_enc); + g_free(filename); +} + +static gchar *make_diff(const gchar *filename, gint cmd) +{ + gchar *std_output = NULL; + gchar *std_error = NULL; + gint exit_code; + gchar *text = NULL; + gchar *dir; + gchar **env = get_cmd_env(cmd, FALSE, filename); + gchar **argv = get_cmd_env(cmd, TRUE, filename); + + if (!argv) + { + if (env) + g_strfreev(env); + return NULL; + } + + if (g_file_test(filename, G_FILE_TEST_IS_DIR)) + { + dir = g_strdup(filename); + } + else + { + dir = g_path_get_dirname(filename); + } + + if (g_spawn_sync(dir, argv, env, G_SPAWN_SEARCH_PATH, NULL, NULL, &std_output, &std_error, &exit_code, NULL)) + { + // CVS dump stuff to stderr when diff nested dirs + if (strcmp(argv[0], "cvs") != 0 && NZV(std_error)) + { + dialogs->show_msgbox(1, + _("%s exited with an error: \n%s."), argv[0], g_strstrip(std_error)); + } + else if (NZV(std_output)) + { + text = std_output; + } + else + { + ui->set_statusbar(FALSE, _("No changes were made.")); + } + } + else + { + ui->set_statusbar(FALSE, + _("Something went really wrong.")); + } + g_free(dir); + g_free(std_error); + g_strfreev(env); + g_strfreev(argv); + return text; +} + + +/* Make a diff from the current directory */ +static void vcdirectory_activated(GtkMenuItem *menuitem, gpointer gdata) +{ + gint idx; + gchar *base_name = NULL; + gchar *locale_filename = NULL; + gchar *text; + + idx = documents->get_cur_idx(); + + g_return_if_fail(DOC_IDX_VALID(idx) && doc_list[idx].file_name != NULL); + + if (doc_list[idx].changed) + { + documents->save_file(idx, FALSE); + } + + locale_filename = utils->get_locale_from_utf8(doc_list[idx].file_name); + base_name = g_path_get_dirname(locale_filename); + + text = make_diff(base_name, VC_COMMAND_DIFF_DIR); + if (text) + { + show_output(text, base_name, NULL); + g_free(text); + } + + g_free(base_name); + g_free(locale_filename); +} + + +/* Callback if menu item for the current project was activated */ +static void vcproject_activated(GtkMenuItem *menuitem, gpointer gdata) +{ + gint idx; + gchar *locale_filename = NULL; + gchar *text; + + idx = documents->get_cur_idx(); + + g_return_if_fail(project != NULL && NZV(project->base_path)); + + if (DOC_IDX_VALID(idx) && doc_list[idx].changed && doc_list[idx].file_name != NULL) + { + documents->save_file(idx, FALSE); + } + + locale_filename = utils->get_locale_from_utf8(project->base_path); + text = make_diff(locale_filename, VC_COMMAND_DIFF_PROJECT); + if (text) + { + show_output(text, project->name, NULL); + g_free(text); + } + g_free(locale_filename); +} + + +/* Callback if menu item for a single file was activated */ +static void vcfile_activated(GtkMenuItem *menuitem, gpointer gdata) +{ + gint idx; + gchar *locale_filename, *text; + + idx = documents->get_cur_idx(); + + g_return_if_fail(DOC_IDX_VALID(idx) && doc_list[idx].file_name != NULL); + + if (doc_list[idx].changed) + { + documents->save_file(idx, FALSE); + } + + locale_filename = utils->get_locale_from_utf8(doc_list[idx].file_name); + + text = make_diff(locale_filename, VC_COMMAND_DIFF_FILE); + if (text) + { + show_output(text, doc_list[idx].file_name, doc_list[idx].encoding); + g_free(text); + } + g_free(locale_filename); +} + + +static GtkWidget *menu_vcdiff_file = NULL; +static GtkWidget *menu_vcdiff_dir = NULL; +static GtkWidget *menu_vcdiff_project = NULL; + +static void update_menu_items() +{ + document *doc; + gboolean have_file; + gboolean have_vc = FALSE; + + doc = documents->get_current(); + have_file = doc && doc->file_name && g_path_is_absolute(doc->file_name); + if (find_cmd_env(VC_COMMAND_DIFF_FILE, TRUE, doc->file_name)) + have_vc = TRUE; + + gtk_widget_set_sensitive(menu_vcdiff_file, have_vc); + gtk_widget_set_sensitive(menu_vcdiff_dir, have_vc); + gtk_widget_set_sensitive(menu_vcdiff_project, + project != NULL && NZV(project->base_path)); +} + + +/* Called by Geany to initialize the plugin */ +void init(GeanyData *data) +{ + GtkWidget *menu_vcdiff = NULL; + GtkWidget *menu_vcdiff_menu = NULL; + GtkTooltips *tooltips = NULL; + + tooltips = gtk_tooltips_new(); + + plugin_fields->flags = PLUGIN_IS_DOCUMENT_SENSITIVE; + + menu_vcdiff = gtk_image_menu_item_new_with_mnemonic(_("_VCdiff")); + gtk_container_add(GTK_CONTAINER(data->tools_menu), menu_vcdiff); + + g_signal_connect((gpointer) menu_vcdiff, "activate", + G_CALLBACK(update_menu_items), NULL); + + menu_vcdiff_menu = gtk_menu_new (); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_vcdiff), menu_vcdiff_menu); + + // Single file + menu_vcdiff_file = gtk_menu_item_new_with_mnemonic(_("From Current _File")); + gtk_container_add(GTK_CONTAINER (menu_vcdiff_menu), menu_vcdiff_file); + gtk_tooltips_set_tip (tooltips, menu_vcdiff_file, + _("Make a diff from the current active file"), NULL); + + g_signal_connect((gpointer) menu_vcdiff_file, "activate", + G_CALLBACK(vcfile_activated), NULL); + + // Directory + menu_vcdiff_dir = gtk_menu_item_new_with_mnemonic(_("From Current _Directory")); + gtk_container_add(GTK_CONTAINER (menu_vcdiff_menu), menu_vcdiff_dir); + gtk_tooltips_set_tip (tooltips, menu_vcdiff_dir, + _("Make a diff from the directory of the current active file"), NULL); + + g_signal_connect((gpointer) menu_vcdiff_dir, "activate", + G_CALLBACK(vcdirectory_activated), NULL); + + // Project + menu_vcdiff_project = gtk_menu_item_new_with_mnemonic(_("From Current _Project")); + gtk_container_add(GTK_CONTAINER (menu_vcdiff_menu), menu_vcdiff_project); + gtk_tooltips_set_tip (tooltips, menu_vcdiff_project, + _("Make a diff from the current project's base path"), NULL); + + g_signal_connect((gpointer) menu_vcdiff_project, "activate", + G_CALLBACK(vcproject_activated), NULL); + + gtk_widget_show_all(menu_vcdiff); + + plugin_fields->menu_item = menu_vcdiff; + plugin_fields->flags = PLUGIN_IS_DOCUMENT_SENSITIVE; +} + + +/* Called by Geany before unloading the plugin. */ +void cleanup() +{ + // remove the menu item added in init() + gtk_widget_destroy(plugin_fields->menu_item); +}