From db6507fac1ab68154682f675e7fd071d22548111 Mon Sep 17 00:00:00 2001 From: James Liggett Date: Sun, 11 May 2008 01:26:47 -0700 Subject: [PATCH] Implement simple revision log UI --- plugins/git/Makefile.am | 12 +- plugins/git/anjuta-git.glade | 209 ++++++++++++- plugins/git/anjuta-git.ui | 6 +- plugins/git/giggle-graph-renderer.c | 547 ++++++++++++++++++++++++++++++++++ plugins/git/giggle-graph-renderer.h | 58 ++++ plugins/git/git-log-command.c | 223 ++++++++++++++ plugins/git/git-log-command.h | 64 ++++ plugins/git/git-log-dialog.c | 392 ++++++++++++++++++++++++ plugins/git/git-log-dialog.h | 44 +++ plugins/git/git-log-message-command.c | 143 +++++++++ plugins/git/git-log-message-command.h | 65 ++++ plugins/git/git-revision.c | 225 ++++++++++++++ plugins/git/git-revision.h | 66 ++++ plugins/git/git-ui-utils.c | 13 +- plugins/git/plugin.c | 29 ++ plugins/git/plugin.h | 2 + 16 files changed, 2085 insertions(+), 13 deletions(-) create mode 100644 plugins/git/giggle-graph-renderer.c create mode 100644 plugins/git/giggle-graph-renderer.h create mode 100644 plugins/git/git-log-command.c create mode 100644 plugins/git/git-log-command.h create mode 100644 plugins/git/git-log-dialog.c create mode 100644 plugins/git/git-log-dialog.h create mode 100644 plugins/git/git-log-message-command.c create mode 100644 plugins/git/git-log-message-command.h create mode 100644 plugins/git/git-revision.c create mode 100644 plugins/git/git-revision.h diff --git a/plugins/git/Makefile.am b/plugins/git/Makefile.am index 5cb9d7eb..a19ed990 100644 --- a/plugins/git/Makefile.am +++ b/plugins/git/Makefile.am @@ -99,7 +99,17 @@ libanjuta_git_la_SOURCES = \ git-checkout-files-command.c \ git-checkout-files-command.h \ git-checkout-files-dialog.c \ - git-checkout-files-dialog.h + git-checkout-files-dialog.h \ + giggle-graph-renderer.c \ + giggle-graph-renderer.h \ + git-log-command.c \ + git-log-command.h \ + git-log-dialog.c \ + git-log-dialog.h \ + git-revision.c \ + git-revision.h \ + git-log-message-command.c \ + git-log-message-command.h libanjuta_git_la_LDFLAGS = $(ANJUTA_PLUGIN_LDFLAGS) diff --git a/plugins/git/anjuta-git.glade b/plugins/git/anjuta-git.glade index 1cfa5e2e..3e065fea 100644 --- a/plugins/git/anjuta-git.glade +++ b/plugins/git/anjuta-git.glade @@ -1,8 +1,9 @@ - + + 600 400 @@ -1542,4 +1543,210 @@ + + + + True + + + True + 0 + GTK_SHADOW_NONE + + + True + 12 + + + True + + + True + 2 + + + True + False + 4 + + + True + True + + + + + + + True + True + True + 0 + + + True + 2 + + + True + gtk-find + + + + + True + View log + + + 1 + + + + + + + False + False + 1 + + + + + False + False + + + + + True + True + Whole project + 0 + True + True + + + False + 1 + + + + + + + + + True + <b>View the Log for File/Folder:</b> + True + + + label_item + + + + + False + + + + + True + True + 300 + True + + + True + 0 + GTK_SHADOW_NONE + + + True + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + True + True + True + + + + + + + + + True + <b>Changes:</b> + True + + + label_item + + + + + False + True + + + + + True + 0 + GTK_SHADOW_NONE + + + True + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + GTK_SHADOW_IN + + + True + True + False + + + + + + + + + True + <b>Log Message:</b> + True + + + label_item + + + + + True + True + + + + + 1 + + + + + diff --git a/plugins/git/anjuta-git.ui b/plugins/git/anjuta-git.ui index fe045db3..87425b50 100644 --- a/plugins/git/anjuta-git.ui +++ b/plugins/git/anjuta-git.ui @@ -9,14 +9,16 @@ + + - + - + diff --git a/plugins/git/giggle-graph-renderer.c b/plugins/git/giggle-graph-renderer.c new file mode 100644 index 00000000..d23ebb64 --- /dev/null +++ b/plugins/git/giggle-graph-renderer.c @@ -0,0 +1,547 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include +#include +#include + +#include "giggle-graph-renderer.h" +#include "git-revision.h" + +#define GET_PRIV(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), GIGGLE_TYPE_GRAPH_RENDERER, GiggleGraphRendererPrivate)) + +/* included padding */ +#define PATH_SPACE(font_size) (font_size + 3) +#define DOT_RADIUS(font_size) (font_size / 2) +#define LINE_WIDTH(font_size) ((font_size / 6) << 1) /* we want the closest even number <= size/3 */ +#define NEXT_COLOR(n_color) ((n_color % (G_N_ELEMENTS (colors) - 1)) + 1) +#define INVALID_COLOR 0 + +typedef struct GiggleGraphRendererPrivate GiggleGraphRendererPrivate; + +struct GiggleGraphRendererPrivate { + gint n_paths; + GHashTable *paths_info; + GitRevision *revision; +}; + +typedef struct GiggleGraphRendererPathState GiggleGraphRendererPathState; + +struct GiggleGraphRendererPathState { + gushort upper_n_color : 8; + gushort lower_n_color : 8; + gushort n_path; +}; + +enum { + PROP_0, + PROP_REVISION, +}; + +static GdkColor colors[] = { + /* invalid color */ + { 0x0, 0x0000, 0x0000, 0x0000 }, + /* light palette */ + { 0x0, 0xfc00, 0xe900, 0x4f00 }, /* butter */ + { 0x0, 0xfc00, 0xaf00, 0x3e00 }, /* orange */ + { 0x0, 0xe900, 0xb900, 0x6e00 }, /* chocolate */ + { 0x0, 0x8a00, 0xe200, 0x3400 }, /* chameleon */ + { 0x0, 0x7200, 0x9f00, 0xcf00 }, /* sky blue */ + { 0x0, 0xad00, 0x7f00, 0xa800 }, /* plum */ + { 0x0, 0xef00, 0x2900, 0x2900 }, /* scarlet red */ +#if 0 + { 0x0, 0xee00, 0xee00, 0xec00 }, /* aluminium */ +#endif + { 0x0, 0x8800, 0x8a00, 0x8500 }, /* no name grey */ + /* medium palette */ + { 0x0, 0xed00, 0xd400, 0x0000 }, /* butter */ + { 0x0, 0xf500, 0x7900, 0x0000 }, /* orange */ + { 0x0, 0xc100, 0x7d00, 0x1100 }, /* chocolate */ + { 0x0, 0x7300, 0xd200, 0x1600 }, /* chameleon */ + { 0x0, 0x3400, 0x6500, 0xa400 }, /* sky blue */ + { 0x0, 0x7500, 0x5000, 0x7b00 }, /* plum */ + { 0x0, 0xcc00, 0x0000, 0x0000 }, /* scarlet red */ +#if 0 + { 0x0, 0xd300, 0xd700, 0xcf00 }, /* aluminium */ +#endif + { 0x0, 0x5500, 0x5700, 0x5300 }, /* no name grey */ + /* dark palette */ + { 0x0, 0xc400, 0xa000, 0x0000 }, /* butter */ + { 0x0, 0xce00, 0x5c00, 0x0000 }, /* orange */ + { 0x0, 0x8f00, 0x5900, 0x0200 }, /* chocolate */ + { 0x0, 0x4e00, 0x9a00, 0x0600 }, /* chameleon */ + { 0x0, 0x2000, 0x4a00, 0x8700 }, /* sky blue */ + { 0x0, 0x5c00, 0x3500, 0x6600 }, /* plum */ + { 0x0, 0xa400, 0x0000, 0x0000 }, /* scarlet red */ +#if 0 + { 0x0, 0xba00, 0xbd00, 0xb600 }, /* aluminium */ +#endif + { 0x0, 0x2e00, 0x3400, 0x3600 }, /* no name grey */ +}; + +static GQuark revision_paths_state_quark; + +static void giggle_graph_renderer_finalize (GObject *object); +static void giggle_graph_renderer_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); +static void giggle_graph_renderer_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); +static void giggle_graph_renderer_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height); +static void giggle_graph_renderer_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + guint flags); + +G_DEFINE_TYPE (GiggleGraphRenderer, giggle_graph_renderer, GTK_TYPE_CELL_RENDERER) + +static void +giggle_graph_renderer_class_init (GiggleGraphRendererClass *class) +{ + GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + cell_class->get_size = giggle_graph_renderer_get_size; + cell_class->render = giggle_graph_renderer_render; + + object_class->finalize = giggle_graph_renderer_finalize; + object_class->set_property = giggle_graph_renderer_set_property; + object_class->get_property = giggle_graph_renderer_get_property; + + g_object_class_install_property ( + object_class, + PROP_REVISION, + g_param_spec_object ("revision", + "revision", + "revision", + GIT_TYPE_REVISION, + G_PARAM_READWRITE)); + + g_type_class_add_private (object_class, + sizeof (GiggleGraphRendererPrivate)); + + revision_paths_state_quark = g_quark_from_static_string ("giggle-revision-paths-state"); +} + +static void +giggle_graph_renderer_init (GiggleGraphRenderer *instance) +{ + instance->_priv = GET_PRIV (instance); +} + +static void +giggle_graph_renderer_finalize (GObject *object) +{ + GiggleGraphRendererPrivate *priv; + + priv = GET_PRIV (object); + + if (priv->paths_info) { + g_hash_table_destroy (priv->paths_info); + } + + G_OBJECT_CLASS (giggle_graph_renderer_parent_class)->finalize (object); +} + +static void +giggle_graph_renderer_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec) +{ + GiggleGraphRendererPrivate *priv = GIGGLE_GRAPH_RENDERER (object)->_priv; + + switch (param_id) { + case PROP_REVISION: + g_value_set_object (value, priv->revision); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + } +} + +static void +giggle_graph_renderer_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + GiggleGraphRendererPrivate *priv = GIGGLE_GRAPH_RENDERER (object)->_priv; + + switch (param_id) { + case PROP_REVISION: + if (priv->revision) { + g_object_unref (priv->revision); + } + priv->revision = GIT_REVISION (g_value_dup_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec); + } +} + +static void +giggle_graph_renderer_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height) +{ + GiggleGraphRendererPrivate *priv; + gint size; + + priv = GIGGLE_GRAPH_RENDERER (cell)->_priv; + size = PANGO_PIXELS (pango_font_description_get_size (widget->style->font_desc)); + + if (height) { + *height = PATH_SPACE (size); + } + + if (width) { + /* the +1 is because we leave half at each side */ + *width = PATH_SPACE (size) * (priv->n_paths + 1); + } + + if (x_offset) { + x_offset = 0; + } + + if (y_offset) { + y_offset = 0; + } +} + +static void +giggle_graph_renderer_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + guint flags) +{ + GiggleGraphRendererPrivate *priv; + GiggleGraphRendererPathState *path_state; + GitRevision *revision; + GArray *paths_state; + GHashTable *table; + cairo_t *cr; + gint x, y, h; + gint cur_pos, pos; + GList *children; + gint size, i; + + priv = GIGGLE_GRAPH_RENDERER (cell)->_priv; + + if (!priv->revision) { + return; + } + + cr = gdk_cairo_create (window); + x = cell_area->x; + y = background_area->y; + h = background_area->height; + revision = priv->revision; + size = PANGO_PIXELS (pango_font_description_get_size (widget->style->font_desc)); + + table = g_hash_table_new (g_direct_hash, g_direct_equal); + paths_state = g_object_get_qdata (G_OBJECT (revision), revision_paths_state_quark); + children = git_revision_get_children (revision); + cur_pos = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision)); + cairo_set_line_width (cr, LINE_WIDTH (size)); + cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND); + + /* paint paths */ + for (i = 0; i < paths_state->len; i++) { + path_state = & g_array_index (paths_state, GiggleGraphRendererPathState, i); + g_hash_table_insert (table, GINT_TO_POINTER ((gint) path_state->n_path), path_state); + pos = path_state->n_path; + + if (path_state->lower_n_color != INVALID_COLOR && + (pos != cur_pos || git_revision_has_parents (revision))) { + gdk_cairo_set_source_color (cr, &colors[path_state->lower_n_color]); + cairo_move_to (cr, x + (pos * PATH_SPACE (size)), y + (h / 2)); + cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y + h); + cairo_stroke (cr); + } + + if (path_state->upper_n_color != INVALID_COLOR) { + gdk_cairo_set_source_color (cr, &colors[path_state->upper_n_color]); + cairo_move_to (cr, x + (pos * PATH_SPACE (size)), y); + cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y + (h / 2)); + cairo_stroke (cr); + } + } + + /* paint connections between paths */ + while (children) { + pos = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, children->data)); + path_state = g_hash_table_lookup (table, GINT_TO_POINTER (pos)); + + if (path_state->upper_n_color != INVALID_COLOR) { + gdk_cairo_set_source_color (cr, &colors[path_state->upper_n_color]); + cairo_move_to (cr, + x + (cur_pos * PATH_SPACE (size)), + y + (h / 2)); + cairo_line_to (cr, + x + (pos * PATH_SPACE (size)), + y + (h / 2)); + + /* redraw the upper part of the path before + * stroking to get a rounded connection + */ + cairo_line_to (cr, x + (pos * PATH_SPACE (size)), y); + + cairo_stroke (cr); + } + + children = children->next; + } + + /* paint circle */ + cairo_set_source_rgb (cr, 0, 0, 0); + cairo_arc (cr, + x + (cur_pos * PATH_SPACE (size)), + y + (h / 2), + DOT_RADIUS (size), 0, 2 * G_PI); + cairo_stroke (cr); + + /* paint internal circle */ + path_state = g_hash_table_lookup (table, GINT_TO_POINTER (cur_pos)); + gdk_cairo_set_source_color (cr, &colors[path_state->lower_n_color]); + cairo_arc (cr, + x + (cur_pos * PATH_SPACE (size)), + y + (h / 2), + DOT_RADIUS (size) - 1, 0, 2 * G_PI); + cairo_fill (cr); + cairo_stroke (cr); + + cairo_destroy (cr); + g_hash_table_destroy (table); +} + +GtkCellRenderer * +giggle_graph_renderer_new (void) +{ + return g_object_new (GIGGLE_TYPE_GRAPH_RENDERER, NULL); +} + +static void +find_free_path (GHashTable *visible_paths, + gint *n_paths, + gint *path) +{ + gint cur_path = 1; + + /* find first path not in list */ + while (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (cur_path))) { + cur_path++; + } + + *path = cur_path; + + /* increment number of paths */ + if (*path > *n_paths) { + *n_paths = *path; + } +} + +static void +get_initial_status_foreach (gpointer key, + gpointer value, + gpointer user_data) +{ + GiggleGraphRendererPathState path_state; + GArray *array; + gint n_color, n_path; + + array = (GArray *) user_data; + n_color = GPOINTER_TO_INT (value); + n_path = GPOINTER_TO_INT (key); + + path_state.n_path = n_path; + path_state.lower_n_color = n_color; + path_state.upper_n_color = n_color; + + g_array_append_val (array, path_state); +} + +static GArray * +get_initial_status (GHashTable *visible_paths) +{ + GArray *array; + guint size; + + size = g_hash_table_size (visible_paths); + array = g_array_sized_new (FALSE, TRUE, sizeof (GiggleGraphRendererPathState), size); + + g_hash_table_foreach (visible_paths, (GHFunc) get_initial_status_foreach, array); + + return array; +} + +static void +free_paths_state (GArray *array) +{ + g_array_free (array, FALSE); +} + +static void +giggle_graph_renderer_calculate_revision_state (GiggleGraphRenderer *renderer, + GitRevision *revision, + GHashTable *visible_paths, + gint *n_color) +{ + GiggleGraphRendererPathState path_state; + GiggleGraphRendererPrivate *priv; + GitRevision *rev; + GArray *paths_state; + GList *children; + gboolean current_path_reused = FALSE; + gboolean update_color; + gint n_path, i; + + priv = renderer->_priv; + children = git_revision_get_children (revision); + update_color = (g_list_length (children) > 1); + paths_state = get_initial_status (visible_paths); + + while (children) { + rev = GIT_REVISION (children->data); + n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, rev)); + + if (!n_path) { + /* there wasn't a path for this revision, choose one */ + if (!current_path_reused) { + n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision)); + current_path_reused = TRUE; + } else { + find_free_path (visible_paths, &priv->n_paths, &n_path); + } + + g_hash_table_insert (priv->paths_info, rev, GINT_TO_POINTER (n_path)); + path_state.lower_n_color = + GPOINTER_TO_INT (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (n_path))); + + if (update_color) { + path_state.upper_n_color = *n_color = NEXT_COLOR (*n_color); + } else { + path_state.upper_n_color = path_state.lower_n_color; + } + } else { + path_state.lower_n_color = + GPOINTER_TO_INT (g_hash_table_lookup (visible_paths, GINT_TO_POINTER (n_path))); + + path_state.upper_n_color = path_state.lower_n_color; + } + + path_state.n_path = n_path; + g_hash_table_insert (visible_paths, GINT_TO_POINTER (n_path), GINT_TO_POINTER ((gint) path_state.upper_n_color)); + g_array_append_val (paths_state, path_state); + + children = children->next; + } + + if (!current_path_reused) { + /* current path is a dead end, remove it from the visible paths table */ + n_path = GPOINTER_TO_INT (g_hash_table_lookup (priv->paths_info, revision)); + g_hash_table_remove (visible_paths, GINT_TO_POINTER (n_path)); + + for (i = 0; i < paths_state->len; i++) { + path_state = g_array_index (paths_state, GiggleGraphRendererPathState, i); + + if (path_state.n_path == n_path) { + path_state.upper_n_color = INVALID_COLOR; + ((GiggleGraphRendererPathState *) paths_state->data)[i] = path_state; + break; + } + } + } + + g_object_set_qdata_full (G_OBJECT (revision), revision_paths_state_quark, + paths_state, (GDestroyNotify) free_paths_state); +} + +void +giggle_graph_renderer_validate_model (GiggleGraphRenderer *renderer, + GtkTreeModel *model, + gint column) +{ + GiggleGraphRendererPrivate *priv; + GtkTreeIter iter; + gint n_children; + gint n_color = 0; + GitRevision *revision; + GHashTable *visible_paths; + GType contained_type; + gint n_path; + + g_return_if_fail (GIGGLE_IS_GRAPH_RENDERER (renderer)); + g_return_if_fail (GTK_IS_TREE_MODEL (model)); + + priv = renderer->_priv; + contained_type = gtk_tree_model_get_column_type (model, column); + + /*g_return_if_fail (contained_type == GIT_TYPE_REVISION);*/ + + if (priv->paths_info) { + g_hash_table_destroy (priv->paths_info); + } + + priv->n_paths = 0; + priv->paths_info = g_hash_table_new (g_direct_hash, g_direct_equal); + visible_paths = g_hash_table_new (g_direct_hash, g_direct_equal); + n_children = gtk_tree_model_iter_n_children (model, NULL); + + while (n_children) { + /* need to calculate state backwards for proper color asignment */ + n_children--; + gtk_tree_model_iter_nth_child (model, &iter, NULL, n_children); + gtk_tree_model_get (model, &iter, column, &revision, -1); + + if (revision) { + if (!git_revision_has_parents (revision)) { + n_color = NEXT_COLOR (n_color); + find_free_path (visible_paths, &priv->n_paths, &n_path); + g_hash_table_insert (priv->paths_info, revision, GINT_TO_POINTER (n_path)); + g_hash_table_insert (visible_paths, GINT_TO_POINTER (n_path), GINT_TO_POINTER (n_color)); + } + + giggle_graph_renderer_calculate_revision_state (renderer, revision, visible_paths, &n_color); + g_object_unref (revision); + } + } + + g_hash_table_destroy (visible_paths); +} diff --git a/plugins/git/giggle-graph-renderer.h b/plugins/git/giggle-graph-renderer.h new file mode 100644 index 00000000..609b9b9d --- /dev/null +++ b/plugins/git/giggle-graph-renderer.h @@ -0,0 +1,58 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2007 Imendio AB + * + * 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., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GIGGLE_GRAPH_RENDERER_H__ +#define __GIGGLE_GRAPH_RENDERER_H__ + +G_BEGIN_DECLS + +#include + +#define GIGGLE_TYPE_GRAPH_RENDERER (giggle_graph_renderer_get_type ()) +#define GIGGLE_GRAPH_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIGGLE_TYPE_GRAPH_RENDERER, GiggleGraphRenderer)) +#define GIGGLE_GRAPH_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIGGLE_TYPE_GRAPH_RENDERER, GiggleGraphRendererClass)) +#define GIGGLE_IS_GRAPH_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIGGLE_TYPE_GRAPH_RENDERER)) +#define GIGGLE_IS_GRAPH_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIGGLE_TYPE_GRAPH_RENDERER)) +#define GIGGLE_GRAPH_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIGGLE_TYPE_GRAPH_RENDERER, GiggleGraphRendererClass)) + +typedef struct GiggleGraphRenderer GiggleGraphRenderer; +typedef struct GiggleGraphRendererClass GiggleGraphRendererClass; + +struct GiggleGraphRenderer { + GtkCellRenderer parent_instance; + + /**/ + gpointer _priv; +}; + +struct GiggleGraphRendererClass { + GtkCellRendererClass parent_class; +}; + +GType giggle_graph_renderer_get_type (void); +GtkCellRenderer *giggle_graph_renderer_new (void); + +void giggle_graph_renderer_validate_model (GiggleGraphRenderer *renderer, + GtkTreeModel *model, + gint column); + +G_END_DECLS + +#endif /* __GIGGLE_GRAPH_RENDERER_H__ */ diff --git a/plugins/git/git-log-command.c b/plugins/git/git-log-command.c new file mode 100644 index 00000000..0c3ed2b1 --- /dev/null +++ b/plugins/git/git-log-command.c @@ -0,0 +1,223 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * Copyright (C) James Liggett 2008 + * + * git-command-test is free software. + * + * You may 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. + * + * git-command-test 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 git-command-test. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#define COMMIT_REGEX "^commit ([[:xdigit:]]{40})" +#define PARENT_REGEX "^parents (.*)" +#define AUTHOR_REGEX "^author (.*)" +#define TIME_REGEX "^time (\\d*)" +#define SHORT_LOG_REGEX "^(?:short log) (.*)" + +#include "git-log-command.h" + +struct _GitLogCommandPriv +{ + GQueue *output_queue; + GHashTable *revisions; + GitRevision *current_revision; + GRegex *commit_regex; + GRegex *parent_regex; + GRegex *author_regex; + GRegex *time_regex; + GRegex *short_log_regex; +}; + +G_DEFINE_TYPE (GitLogCommand, git_log_command, GIT_TYPE_COMMAND); + +static void +git_log_command_init (GitLogCommand *self) +{ + self->priv = g_new0 (GitLogCommandPriv, 1); + self->priv->output_queue = g_queue_new (); + self->priv->revisions = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->priv->commit_regex = g_regex_new (COMMIT_REGEX, 0, 0, NULL); + self->priv->parent_regex = g_regex_new (PARENT_REGEX, 0, 0, NULL); + self->priv->author_regex = g_regex_new (AUTHOR_REGEX, 0, 0, NULL); + self->priv->time_regex = g_regex_new (TIME_REGEX, 0, 0, NULL); + self->priv->short_log_regex = g_regex_new (SHORT_LOG_REGEX, 0, 0, NULL); +} + +static void +git_log_command_finalize (GObject *object) +{ + GitLogCommand *self; + GList *current_output; + + self = GIT_LOG_COMMAND (object); + current_output = self->priv->output_queue->head; + + while (current_output) + { + g_object_unref (current_output->data); + current_output = g_list_next (current_output); + } + + g_queue_free (self->priv->output_queue); + g_hash_table_destroy (self->priv->revisions); + g_regex_unref (self->priv->commit_regex); + g_regex_unref (self->priv->parent_regex); + g_regex_unref (self->priv->author_regex); + g_regex_unref (self->priv->time_regex); + g_regex_unref (self->priv->short_log_regex); + g_free (self->priv); + + G_OBJECT_CLASS (git_log_command_parent_class)->finalize (object); +} + +static guint +git_log_command_run (AnjutaCommand *command) +{ + git_command_add_arg (GIT_COMMAND (command), "rev-list"); + git_command_add_arg (GIT_COMMAND (command), "--topo-order"); + git_command_add_arg (GIT_COMMAND (command), "--pretty=format:parents %P%n" + "author %an%n" + "time %at%n" + "short log %s%n" + "\x0c"); + git_command_add_arg (GIT_COMMAND (command), "HEAD"); + + return 0; +} + +static void +git_log_command_handle_output (GitCommand *git_command, const gchar *output) +{ + GitLogCommand *self; + GMatchInfo *match_info; + gchar *commit_sha; + gchar *parents; + gchar **parent_shas; + gint i; + GitRevision *parent_revision; + gchar *author; + gchar *time; + gchar *short_log; + + self = GIT_LOG_COMMAND (git_command); + + /* Entries are delimited by the hex value 0x0c */ + if (*output == 0x0c && self->priv->current_revision) + { + g_queue_push_tail (self->priv->output_queue, + self->priv->current_revision); + anjuta_command_notify_data_arrived (ANJUTA_COMMAND (git_command)); + } + + if (g_regex_match (self->priv->commit_regex, output, 0, &match_info)) + { + commit_sha = g_match_info_fetch (match_info, 1); + + self->priv->current_revision = g_hash_table_lookup (self->priv->revisions, + commit_sha); + + if (!self->priv->current_revision) + { + self->priv->current_revision = git_revision_new (); + git_revision_set_sha (self->priv->current_revision, commit_sha); + g_hash_table_insert (self->priv->revisions, g_strdup (commit_sha), + g_object_ref (self->priv->current_revision)); + } + + g_free (commit_sha); + } + else if (g_regex_match (self->priv->parent_regex, output, 0, &match_info)) + { + parents = g_match_info_fetch (match_info, 1); + parent_shas = g_strsplit (parents, " ", -1); + + for (i = 0; parent_shas[i]; i++) + { + parent_revision = g_hash_table_lookup (self->priv->revisions, + parent_shas[i]); + + if (!parent_revision) + { + parent_revision = git_revision_new (); + git_revision_set_sha (parent_revision, parent_shas[i]); + g_hash_table_insert (self->priv->revisions, + g_strdup (parent_shas[i]), + g_object_ref (parent_revision)); + } + + git_revision_add_child (parent_revision, + self->priv->current_revision); + } + + g_strfreev (parent_shas); + } + else if (g_regex_match (self->priv->author_regex, output, 0, &match_info)) + { + author = g_match_info_fetch (match_info, 1); + git_revision_set_author (self->priv->current_revision, author); + + g_free (author); + } + else if (g_regex_match (self->priv->time_regex, output, 0, &match_info)) + { + time = g_match_info_fetch (match_info, 1); + git_revision_set_date (self->priv->current_revision, atol (time)); + + g_free (time); + } + else if (g_regex_match (self->priv->short_log_regex, output, 0, + &match_info)) + { + short_log = g_match_info_fetch (match_info, 1); + git_revision_set_short_log (self->priv->current_revision, short_log); + + g_free (short_log); + } + + if (match_info) + g_match_info_free (match_info); + +} + +static void +git_log_command_class_init (GitLogCommandClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + GitCommandClass* parent_class = GIT_COMMAND_CLASS (klass); + AnjutaCommandClass *command_class = ANJUTA_COMMAND_CLASS (klass); + + object_class->finalize = git_log_command_finalize; + parent_class->output_handler = git_log_command_handle_output; + command_class->run = git_log_command_run; +} + + +GitLogCommand * +git_log_command_new (const gchar *working_directory) +{ + return g_object_new (GIT_TYPE_LOG_COMMAND, + "working-directory", working_directory, + "single-line-output", TRUE, + NULL); +} + +GQueue * +git_log_command_get_output_queue (GitLogCommand *self) +{ + return self->priv->output_queue; +} diff --git a/plugins/git/git-log-command.h b/plugins/git/git-log-command.h new file mode 100644 index 00000000..08e73e05 --- /dev/null +++ b/plugins/git/git-log-command.h @@ -0,0 +1,64 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * Copyright (C) James Liggett 2008 + * + * git-command-test is free software. + * + * You may 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. + * + * git-command-test 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 git-command-test. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#ifndef _GIT_LOG_COMMAND_H_ +#define _GIT_LOG_COMMAND_H_ + +#include +#include +#include "git-command.h" +#include "git-revision.h" + +G_BEGIN_DECLS + +#define GIT_TYPE_LOG_COMMAND (git_log_command_get_type ()) +#define GIT_LOG_COMMAND(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIT_TYPE_LOG_COMMAND, GitLogCommand)) +#define GIT_LOG_COMMAND_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIT_TYPE_LOG_COMMAND, GitLogCommandClass)) +#define GIT_IS_LOG_COMMAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIT_TYPE_LOG_COMMAND)) +#define GIT_IS_LOG_COMMAND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIT_TYPE_LOG_COMMAND)) +#define GIT_LOG_COMMAND_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIT_TYPE_LOG_COMMAND, GitLogCommandClass)) + +typedef struct _GitLogCommandClass GitLogCommandClass; +typedef struct _GitLogCommand GitLogCommand; +typedef struct _GitLogCommandPriv GitLogCommandPriv; + +struct _GitLogCommandClass +{ + GitCommandClass parent_class; +}; + +struct _GitLogCommand +{ + GitCommand parent_instance; + + GitLogCommandPriv *priv; +}; + +GType git_log_command_get_type (void) G_GNUC_CONST; +GitLogCommand *git_log_command_new (const gchar *working_directory); +GQueue *git_log_command_get_output_queue (GitLogCommand *self); + +G_END_DECLS + +#endif /* _GIT_LOG_COMMAND_H_ */ diff --git a/plugins/git/git-log-dialog.c b/plugins/git/git-log-dialog.c new file mode 100644 index 00000000..50307ab1 --- /dev/null +++ b/plugins/git/git-log-dialog.c @@ -0,0 +1,392 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * anjuta + * Copyright (C) James Liggett 2007 + * + * anjuta is free software. + * + * You may 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. + * + * anjuta 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 anjuta. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#include "git-log-dialog.h" + +enum +{ + COL_REVISION, + + NUM_COLS +}; + +typedef struct +{ + Git *plugin; + GladeXML *gxml; + GtkListStore *list_store; + GtkCellRenderer *graph_renderer; + gchar *path; +} LogData; + +static void +author_cell_function (GtkTreeViewColumn *column, GtkCellRenderer *renderer, + GtkTreeModel *model, GtkTreeIter *iter, + gpointer user_data) +{ + GitRevision *revision; + gchar *author; + + gtk_tree_model_get (model, iter, COL_REVISION, &revision, -1); + author = git_revision_get_author (revision); + + g_object_unref (revision); + + g_object_set (renderer, "text", author, NULL); + + g_free (author); +} + +static void +date_cell_function (GtkTreeViewColumn *column, GtkCellRenderer *renderer, + GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) +{ + GitRevision *revision; + gchar *date; + + gtk_tree_model_get (model, iter, COL_REVISION, &revision, -1); + date = git_revision_get_formatted_date (revision); + + g_object_unref (revision); + + g_object_set (renderer, "text", date, NULL); + + g_free (date); +} + +static void +short_log_cell_function (GtkTreeViewColumn *column, GtkCellRenderer *renderer, + GtkTreeModel *model, GtkTreeIter *iter, + gpointer user_data) +{ + GitRevision *revision; + gchar *short_log; + + gtk_tree_model_get (model, iter, COL_REVISION, &revision, -1); + short_log = git_revision_get_short_log (revision); + + g_object_unref (revision); + + g_object_set (renderer, "text", short_log, NULL); + + g_free (short_log); +} + +static void +create_columns (LogData *data) +{ + GtkWidget *log_changes_view; + gint font_size; + GtkTreeViewColumn *column; + GtkCellRenderer *renderer; + + log_changes_view = glade_xml_get_widget (data->gxml, "log_changes_view"); + font_size = PANGO_PIXELS (pango_font_description_get_size (GTK_WIDGET (log_changes_view)->style->font_desc)); + + /* Graph */ + column = gtk_tree_view_column_new (); + gtk_tree_view_append_column (GTK_TREE_VIEW (log_changes_view), column); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED); + gtk_tree_view_column_set_min_width (column, font_size * 10); + gtk_tree_view_column_set_resizable (column, TRUE); + gtk_tree_view_column_pack_start (column, data->graph_renderer, TRUE); + gtk_tree_view_column_add_attribute (column, data->graph_renderer, + "revision", COL_REVISION); + gtk_tree_view_column_set_title (column, "Graph"); + + /* Short log */ + column = gtk_tree_view_column_new (); + gtk_tree_view_append_column (GTK_TREE_VIEW (log_changes_view), column); + renderer = gtk_cell_renderer_text_new (); + g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL); + gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED); + gtk_tree_view_column_set_min_width (column, font_size * 10); + gtk_tree_view_column_set_resizable (column, TRUE); + gtk_tree_view_column_set_expand (column, TRUE); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_cell_data_func (column, renderer, + (GtkTreeCellDataFunc) short_log_cell_function, + NULL, NULL); + gtk_tree_view_column_set_title (column, "Short log"); + + /* Author */ + column = gtk_tree_view_column_new (); + gtk_tree_view_append_column (GTK_TREE_VIEW (log_changes_view), column); + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_set_resizable (column, TRUE); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_cell_data_func (column, renderer, + (GtkTreeCellDataFunc) author_cell_function, + NULL, NULL); + gtk_tree_view_column_set_title (column, "Author"); + + /* Date */ + column = gtk_tree_view_column_new (); + gtk_tree_view_append_column (GTK_TREE_VIEW (log_changes_view), column); + renderer = gtk_cell_renderer_text_new (); + gtk_tree_view_column_set_resizable (column, TRUE); + gtk_tree_view_column_pack_start (column, renderer, TRUE); + gtk_tree_view_column_set_cell_data_func (column, renderer, + (GtkTreeCellDataFunc) date_cell_function, + NULL, NULL); + gtk_tree_view_column_set_title (column, "Date"); + + gtk_tree_view_set_model (GTK_TREE_VIEW (log_changes_view), + GTK_TREE_MODEL (data->list_store)); + g_object_unref (data->list_store); + +} + +static void +on_log_command_finished (AnjutaCommand *command, guint return_code, + LogData *data) +{ + GtkWidget *log_changes_view; + GQueue *queue; + GtkTreeIter iter; + GitRevision *revision; + + log_changes_view = glade_xml_get_widget (data->gxml, "log_changes_view"); + + g_object_ref (data->list_store); + gtk_tree_view_set_model (GTK_TREE_VIEW (log_changes_view), NULL); + + queue = git_log_command_get_output_queue (GIT_LOG_COMMAND (command)); + + while (g_queue_peek_head (queue)) + { + revision = g_queue_pop_head (queue); + + gtk_list_store_append (data->list_store, &iter); + gtk_list_store_set (data->list_store, &iter, COL_REVISION, revision, -1); + g_object_unref (revision); + } + + giggle_graph_renderer_validate_model (GIGGLE_GRAPH_RENDERER (data->graph_renderer), + GTK_TREE_MODEL (data->list_store), + COL_REVISION); + gtk_tree_view_set_model (GTK_TREE_VIEW (log_changes_view), + GTK_TREE_MODEL (data->list_store)); + g_object_unref (data->list_store); + + g_object_unref (command); +} + +static void +on_view_log_button_clicked (GtkButton *button, LogData *data) +{ + GitLogCommand *log_command; + gint pulse_timer_id; + + log_command = git_log_command_new (data->plugin->project_root_directory); + + gtk_list_store_clear (data->list_store); + + pulse_timer_id = status_bar_progress_pulse (data->plugin, + _("Git: Retrieving" + " log...")); + + g_signal_connect (G_OBJECT (log_command), "command-finished", + G_CALLBACK (stop_status_bar_progress_pulse), + GUINT_TO_POINTER (pulse_timer_id)); + + g_signal_connect (G_OBJECT (log_command), "command-finished", + G_CALLBACK (on_log_command_finished), + data); + + anjuta_command_start (ANJUTA_COMMAND (log_command)); + +} + +static void +on_log_vbox_destroy (GtkObject *log_vbox, LogData *data) +{ + g_free (data->path); + g_object_unref (data->gxml); + g_free (data); +} + +static void +on_log_message_command_finished (AnjutaCommand *command, guint return_code, + LogData *data) +{ + GtkWidget *log_text_view; + GtkTextBuffer *buffer; + gchar *log_message; + + log_text_view = glade_xml_get_widget (data->gxml, "log_text_view"); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (log_text_view)); + log_message = git_log_message_command_get_message (GIT_LOG_MESSAGE_COMMAND (command)); + + gtk_text_buffer_set_text (buffer, log_message, strlen (log_message)); + + g_free (log_message); + g_object_unref (command); +} + +static gboolean +on_log_changes_view_row_selected (GtkTreeSelection *selection, + GtkTreeModel *model, + GtkTreePath *path, + gboolean path_currently_selected, + LogData *data) +{ + GtkTreeIter iter; + GitRevision *revision; + gchar *sha; + GitLogMessageCommand *log_message_command; + + if (!path_currently_selected) + { + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, COL_REVISION, &revision, -1); + sha = git_revision_get_sha (revision); + + log_message_command = git_log_message_command_new (data->plugin->project_root_directory, + sha); + + g_free (sha); + g_object_unref (revision); + + g_signal_connect (G_OBJECT (log_message_command), "command-finished", + G_CALLBACK (on_log_message_command_finished), + data); + + anjuta_command_start (ANJUTA_COMMAND (log_message_command)); + } + + return TRUE; +} + +GtkWidget * +git_log_window_create (Git *plugin) +{ + LogData *data; + GtkWidget *log_window; + GtkWidget *log_vbox; + GtkWidget *log_changes_view; + GtkWidget *view_log_button; + GtkWidget *whole_project_check; + GtkWidget *path_entry; + GtkTreeSelection *selection; + + data = g_new0 (LogData, 1); + data->gxml = glade_xml_new (GLADE_FILE, "log_window", NULL); + + data->plugin = plugin; + data->path = NULL; + data->graph_renderer = giggle_graph_renderer_new (); + + log_window = glade_xml_get_widget (data->gxml, "log_window"); + log_vbox = glade_xml_get_widget (data->gxml, "log_vbox"); + log_changes_view = glade_xml_get_widget (data->gxml, "log_changes_view"); + whole_project_check = glade_xml_get_widget (data->gxml, + "whole_project_check"); + path_entry = glade_xml_get_widget (data->gxml, "path_entry"); + view_log_button = glade_xml_get_widget (data->gxml, + "view_log_button"); + + g_object_set_data (G_OBJECT (log_vbox), "log-data", data); + + g_signal_connect (G_OBJECT (view_log_button), "clicked", + G_CALLBACK (on_view_log_button_clicked), + data); + + g_object_set_data (G_OBJECT (whole_project_check), "file-entry", + path_entry); + g_signal_connect (G_OBJECT (whole_project_check), "toggled", + G_CALLBACK (on_whole_project_toggled), plugin); + + data->list_store = gtk_list_store_new (NUM_COLS, + G_TYPE_OBJECT); + create_columns (data); + + selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (log_changes_view)); + gtk_tree_selection_set_select_function (selection, + (GtkTreeSelectionFunc) on_log_changes_view_row_selected, + data, NULL); + + g_signal_connect (G_OBJECT (log_vbox), "destroy", + G_CALLBACK (on_log_vbox_destroy), + data); + + g_object_ref (log_vbox); + gtk_container_remove (GTK_CONTAINER (log_window), log_vbox); + gtk_widget_destroy (log_window); + + return log_vbox; +} + +void +on_menu_git_log (GtkAction *action, Git *plugin) +{ + anjuta_shell_present_widget (ANJUTA_PLUGIN (plugin)->shell, + plugin->log_viewer, NULL); +} + +/* TODO: Enable when FM support is implemented */ +#if 0 +void +on_fm_subversion_log (GtkAction *action, Git *plugin) +{ + GtkWidget *path_text_entry; + + path_text_entry = glade_xml_get_widget (plugin->log_gxml, + "path_text_entry"); + + gtk_entry_set_text (GTK_ENTRY path_entry), + plugin->fm_current_filename); + + anjuta_shell_present_widget (ANJUTA_PLUGIN (plugin)->shell, + plugin->log_viewer, NULL); +} +#endif + +void +git_log_window_clear (Git *plugin) +{ + LogData *data; + GtkWidget *log_text_view; + GtkTextBuffer *buffer; + + data = g_object_get_data (G_OBJECT (plugin->log_viewer), "log-data"); + log_text_view = glade_xml_get_widget (data->gxml, "log_text_view"); + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (log_text_view)); + + gtk_list_store_clear (data->list_store); + gtk_text_buffer_set_text (buffer, "", 0); +} + +void +git_log_set_whole_project_sensitive (Git *plugin, gboolean sensitive) +{ + LogData *data; + GtkWidget *whole_project_check; + + data = g_object_get_data (G_OBJECT (plugin->log_viewer), "log-data"); + whole_project_check = glade_xml_get_widget (data->gxml, + "whole_project_check"); + + gtk_widget_set_sensitive (whole_project_check, sensitive); +} diff --git a/plugins/git/git-log-dialog.h b/plugins/git/git-log-dialog.h new file mode 100644 index 00000000..5dae767d --- /dev/null +++ b/plugins/git/git-log-dialog.h @@ -0,0 +1,44 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * anjuta + * Copyright (C) James Liggett 2007 + * + * anjuta is free software. + * + * You may 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. + * + * anjuta 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 anjuta. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#ifndef GIT_LOG_DIALOG_H +#define GIT_LOG_DIALOG_H + +#include "git-ui-utils.h" +#include "git-log-command.h" +#include "git-log-message-command.h" +#include "giggle-graph-renderer.h" + +void on_menu_git_log (GtkAction* action, Git *plugin); + +/* TODO: Enable when FM support is implemented */ +#if 0 +void on_fm_git_log (GtkAction *action, Git *plugin); +#endif + +GtkWidget *git_log_window_create (Git *plugin); +void git_log_window_clear (Git *plugin); +void git_log_set_whole_project_sensitive (Git *plugin, gboolean sensitive); + +#endif diff --git a/plugins/git/git-log-message-command.c b/plugins/git/git-log-message-command.c new file mode 100644 index 00000000..b0e86119 --- /dev/null +++ b/plugins/git/git-log-message-command.c @@ -0,0 +1,143 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * Copyright (C) James Liggett 2008 + * + * git-command-test is free software. + * + * You may 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. + * + * git-command-test 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 git-command-test. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#define COMMITTER_REGEX "^committer" +#define COMMIT_REGEX "^commit" + +#include "git-log-message-command.h" + +struct _GitLogMessageCommandPriv +{ + gchar *sha; + GRegex *committer_regex; + GRegex *commit_regex; + GString *log_message; + gboolean found_committer_line; + gboolean found_message; +}; + +G_DEFINE_TYPE (GitLogMessageCommand, git_log_message_command, GIT_TYPE_COMMAND); + +static void +git_log_message_command_init (GitLogMessageCommand *self) +{ + self->priv = g_new0 (GitLogMessageCommandPriv, 1); + + self->priv->committer_regex = g_regex_new (COMMITTER_REGEX, 0, 0, NULL); + self->priv->commit_regex = g_regex_new (COMMIT_REGEX, 0, 0, NULL); + self->priv->log_message = g_string_new (""); +} + +static void +git_log_message_command_finalize (GObject *object) +{ + GitLogMessageCommand *self; + + self = GIT_LOG_MESSAGE_COMMAND (object); + + g_regex_unref (self->priv->committer_regex); + g_regex_unref (self->priv->commit_regex); + g_string_free (self->priv->log_message, TRUE); + + G_OBJECT_CLASS (git_log_message_command_parent_class)->finalize (object); +} + +static guint +git_log_message_command_run (AnjutaCommand *command) +{ + GitLogMessageCommand *self; + gchar *revision; + + self = GIT_LOG_MESSAGE_COMMAND (command); + + revision = g_strdup_printf ("%s^..%s", self->priv->sha, self->priv->sha); + + git_command_add_arg (GIT_COMMAND (command), "rev-list"); + git_command_add_arg (GIT_COMMAND (command), "--pretty=raw"); + git_command_add_arg (GIT_COMMAND (command), revision); + + g_free (revision); + + return 0; +} + +static void +git_log_message_command_handle_output (GitCommand *git_command, + const gchar *output) +{ + GitLogMessageCommand *self; + + self = GIT_LOG_MESSAGE_COMMAND (git_command); + + /* It is possible that we could encounter multiple objects, usually with + * merges. */ + if (g_regex_match (self->priv->commit_regex, output, 0, NULL)) + { + self->priv->found_message = FALSE; + self->priv->found_committer_line = FALSE; + } + + if (self->priv->found_message) + g_string_append (self->priv->log_message, output); + + if (self->priv->found_committer_line) + self->priv->found_message = TRUE; + else if (g_regex_match (self->priv->committer_regex, output, 0, NULL)) + self->priv->found_committer_line = TRUE; /* Skip next line */ +} + +static void +git_log_message_command_class_init (GitLogMessageCommandClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + GitCommandClass* parent_class = GIT_COMMAND_CLASS (klass); + AnjutaCommandClass *command_class = ANJUTA_COMMAND_CLASS (klass); + + object_class->finalize = git_log_message_command_finalize; + parent_class->output_handler = git_log_message_command_handle_output; + command_class->run = git_log_message_command_run; +} + + +GitLogMessageCommand * +git_log_message_command_new (const gchar *working_directory, const gchar *sha) +{ + GitLogMessageCommand *self; + + self = g_object_new (GIT_TYPE_LOG_MESSAGE_COMMAND, + "working-directory", working_directory, + "single-line-output", TRUE, + NULL); + + self->priv->sha = g_strdup (sha); + + return self; +} + +gchar * +git_log_message_command_get_message (GitLogMessageCommand *self) +{ + return g_strdup (g_strchomp (self->priv->log_message->str)); +} + diff --git a/plugins/git/git-log-message-command.h b/plugins/git/git-log-message-command.h new file mode 100644 index 00000000..ab5150c8 --- /dev/null +++ b/plugins/git/git-log-message-command.h @@ -0,0 +1,65 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * Copyright (C) James Liggett 2008 + * + * git-command-test is free software. + * + * You may 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. + * + * git-command-test 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 git-command-test. If not, write to: + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. + */ + +#ifndef _GIT_LOG_MESSAGE_COMMAND_H_ +#define _GIT_LOG_MESSAGE_COMMAND_H_ + +#include +#include +#include "git-command.h" +#include "git-revision.h" + +G_BEGIN_DECLS + +#define GIT_TYPE_LOG_MESSAGE_COMMAND (git_log_message_command_get_type ()) +#define GIT_LOG_MESSAGE_COMMAND(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIT_TYPE_LOG_MESSAGE_COMMAND, GitLogMessageCommand)) +#define GIT_LOG_MESSAGE_COMMAND_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIT_TYPE_LOG_MESSAGE_COMMAND, GitLogMessageCommandClass)) +#define GIT_IS_LOG_MESSAGE_COMMAND(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIT_TYPE_LOG_MESSAGE_COMMAND)) +#define GIT_IS_LOG_MESSAGE_COMMAND_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIT_TYPE_LOG_MESSAGE_COMMAND)) +#define GIT_LOG_MESSAGE_COMMAND_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIT_TYPE_LOG_MESSAGE_COMMAND, GitLogMessageCommandClass)) + +typedef struct _GitLogMessageCommandClass GitLogMessageCommandClass; +typedef struct _GitLogMessageCommand GitLogMessageCommand; +typedef struct _GitLogMessageCommandPriv GitLogMessageCommandPriv; + +struct _GitLogMessageCommandClass +{ + GitCommandClass parent_class; +}; + +struct _GitLogMessageCommand +{ + GitCommand parent_instance; + + GitLogMessageCommandPriv *priv; +}; + +GType git_log_message_command_get_type (void) G_GNUC_CONST; +GitLogMessageCommand *git_log_message_command_new (const gchar *working_directory, + const gchar *sha); +gchar *git_log_message_command_get_message (GitLogMessageCommand *self); + +G_END_DECLS + +#endif /* _GIT_LOG_MESSAGE_COMMAND_H_ */ diff --git a/plugins/git/git-revision.c b/plugins/git/git-revision.c new file mode 100644 index 00000000..a9f6e2f0 --- /dev/null +++ b/plugins/git/git-revision.c @@ -0,0 +1,225 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * + * git-command-test 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 3 of the License, or + * (at your option) any later version. + * + * git-command-test 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, see . + */ + +#include "git-revision.h" + +struct _GitRevisionPriv +{ + gchar *sha; + gchar *author; + gchar *date; + gchar *short_log; + GList *children; + gboolean has_parents; +}; + +G_DEFINE_TYPE (GitRevision, git_revision, G_TYPE_OBJECT); + +static void +git_revision_init (GitRevision *self) +{ + self->priv = g_new0 (GitRevisionPriv, 1); +} + +static void +git_revision_finalize (GObject *object) +{ + GitRevision *self; + GList *current_child; + + self = GIT_REVISION (object); + + g_free (self->priv->sha); + g_free (self->priv->author); + g_free (self->priv->date); + g_free (self->priv->short_log); + + current_child = self->priv->children; + + while (current_child) + { + g_object_unref (current_child->data); + current_child = g_list_next (current_child); + } + + g_list_free (self->priv->children); + g_free (self->priv); + + G_OBJECT_CLASS (git_revision_parent_class)->finalize (object); +} + +static void +git_revision_class_init (GitRevisionClass *klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = git_revision_finalize; +} + +GitRevision * +git_revision_new (void) +{ + return g_object_new (GIT_TYPE_REVISION, NULL); +} + +void +git_revision_set_sha (GitRevision *self, const gchar *sha) +{ + g_free (self->priv->sha); + self->priv->sha = g_strdup (sha); +} + +gchar * +git_revision_get_sha (GitRevision *self) +{ + return g_strdup (self->priv->sha); +} + +void +git_revision_set_author (GitRevision *self, const gchar *author) +{ + g_free (self->priv->author); + self->priv->author = g_strdup (author); +} + +gchar * +git_revision_get_author (GitRevision *self) +{ + return g_strdup (self->priv->author); +} + +void +git_revision_set_short_log (GitRevision *self, const gchar *short_log) +{ + g_free (self->priv->short_log); + self->priv->short_log = g_strdup (short_log); + + g_strchug (self->priv->short_log); +} + +gchar * +git_revision_get_short_log (GitRevision *self) +{ + return g_strdup (self->priv->short_log); +} + +static const gchar * +git_revision_get_time_format (struct tm *revision_time) +{ + struct tm *tm; + time_t t1, t2; + + t1 = mktime ((struct tm *) revision_time); + + /* check whether it's ahead in time */ + time (&t2); + if (t1 > t2) + { + return "%c"; + } + + /* check whether it's as fresh as today's bread */ + t2 = time (NULL); + tm = localtime (&t2); + tm->tm_sec = tm->tm_min = tm->tm_hour = 0; + t2 = mktime (tm); + + if (t1 > t2) + { + /* TRANSLATORS: it's a strftime format string */ + return "%I:%M %p"; + } + + /* check whether it's older than a week */ + t2 = time (NULL); + tm = localtime (&t2); + tm->tm_sec = tm->tm_min = tm->tm_hour = 0; + t2 = mktime (tm); + + t2 -= 60 * 60 * 24 * 6; /* substract 1 week */ + + if (t1 > t2) + { + /* TRANSLATORS: it's a strftime format string */ + return "%a %I:%M %p"; + } + + /* check whether it's more recent than the new year hangover */ + t2 = time (NULL); + tm = localtime (&t2); + tm->tm_sec = tm->tm_min = tm->tm_hour = tm->tm_mon = 0; + tm->tm_mday = 1; + t2 = mktime (tm); + + if (t1 > t2) + { + /* TRANSLATORS: it's a strftime format string */ + return "%b %d %I:%M %p"; + } + + /* it's older */ + /* TRANSLATORS: it's a strftime format string */ + return "%b %d %Y"; +} + +void +git_revision_set_date (GitRevision *self, time_t unix_time) +{ + struct tm time; + const gchar *format; + gchar buffer[256]; + + localtime_r (&unix_time, &time); + format = git_revision_get_time_format (&time); + strftime (buffer, sizeof (buffer), format, &time); + + g_free (self->priv->date); + self->priv->date = g_strdup (buffer); +} + +gchar * +git_revision_get_formatted_date (GitRevision *self) +{ + return g_strdup (self->priv->date); +} + +void +git_revision_add_child (GitRevision *self, GitRevision *child) +{ + self->priv->children = g_list_prepend (self->priv->children, + g_object_ref (child)); + git_revision_set_has_parents (child, TRUE); +} + +GList * +git_revision_get_children (GitRevision *self) +{ + return self->priv->children; +} + +void +git_revision_set_has_parents (GitRevision *self, gboolean has_parents) +{ + self->priv->has_parents = has_parents; +} + +gboolean +git_revision_has_parents (GitRevision *self) +{ + return self->priv->has_parents; +} diff --git a/plugins/git/git-revision.h b/plugins/git/git-revision.h new file mode 100644 index 00000000..9db911a1 --- /dev/null +++ b/plugins/git/git-revision.h @@ -0,0 +1,66 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * git-command-test + * + * git-command-test 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 3 of the License, or + * (at your option) any later version. + * + * git-command-test 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, see . + */ + +#ifndef _GIT_REVISION_H_ +#define _GIT_REVISION_H_ + +#include + +G_BEGIN_DECLS + +#define GIT_TYPE_REVISION (git_revision_get_type ()) +#define GIT_REVISION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIT_TYPE_REVISION, GitRevision)) +#define GIT_REVISION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIT_TYPE_REVISION, GitRevisionClass)) +#define GIT_IS_REVISION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIT_TYPE_REVISION)) +#define GIT_IS_REVISION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIT_TYPE_REVISION)) +#define GIT_REVISION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIT_TYPE_REVISION, GitRevisionClass)) + +typedef struct _GitRevisionClass GitRevisionClass; +typedef struct _GitRevision GitRevision; +typedef struct _GitRevisionPriv GitRevisionPriv; + +struct _GitRevisionClass +{ + GObjectClass parent_class; +}; + +struct _GitRevision +{ + GObject parent_instance; + + GitRevisionPriv *priv; +}; + +GType git_revision_get_type (void) G_GNUC_CONST; +GitRevision *git_revision_new (void); +void git_revision_set_sha (GitRevision *self, const gchar *sha); +gchar *git_revision_get_sha (GitRevision *self); +void git_revision_set_author (GitRevision *self, const gchar *author); +gchar *git_revision_get_author (GitRevision *self); +void git_revision_set_short_log (GitRevision *self, const gchar *short_log); +gchar *git_revision_get_short_log (GitRevision *self); +void git_revision_set_date (GitRevision *self, time_t unix_time); +gchar *git_revision_get_formatted_date (GitRevision *self); +void git_revision_add_child (GitRevision *self, GitRevision *child); +GList *git_revision_get_children (GitRevision *self); +void git_revision_set_has_parents (GitRevision *self, gboolean has_parents); +gboolean git_revision_has_parents (GitRevision *self); + +G_END_DECLS + +#endif /* _GIT_REVISION_H_ */ diff --git a/plugins/git/git-ui-utils.c b/plugins/git/git-ui-utils.c index 37e99f9a..6c99c7e7 100644 --- a/plugins/git/git-ui-utils.c +++ b/plugins/git/git-ui-utils.c @@ -345,17 +345,12 @@ init_whole_project (Git *plugin, GtkWidget* project, gboolean active) void on_whole_project_toggled (GtkToggleButton* project, Git *plugin) { - GtkEntry* file_entry; + GtkWidget *path_entry; - file_entry = g_object_get_data (G_OBJECT (project), "file_entry"); + path_entry = g_object_get_data (G_OBJECT (project), "file-entry"); - if (gtk_toggle_button_get_active(project) && plugin->project_root_directory) - { - gtk_entry_set_text (file_entry, plugin->project_root_directory); - gtk_widget_set_sensitive(GTK_WIDGET(file_entry), FALSE); - } - else - gtk_widget_set_sensitive(GTK_WIDGET(file_entry), TRUE); + gtk_widget_set_sensitive (path_entry, + !gtk_toggle_button_get_active (project)); } void diff --git a/plugins/git/plugin.c b/plugins/git/plugin.c index d7cf0db6..4c7a9fdd 100644 --- a/plugins/git/plugin.c +++ b/plugins/git/plugin.c @@ -31,6 +31,7 @@ #include "git-delete-branch-dialog.h" #include "git-unstage-dialog.h" #include "git-checkout-files-dialog.h" +#include "git-log-dialog.h" #define UI_FILE PACKAGE_DATA_DIR"/ui/anjuta-git.ui" @@ -78,6 +79,14 @@ static GtkActionEntry actions_git[] = { G_CALLBACK (on_menu_git_resolve) /* action callback */ }, { + "ActionGitLog", /* Action name */ + GTK_STOCK_ZOOM_100, /* Stock icon, if any */ + N_("_View log..."), /* Display label */ + NULL, /* short-cut */ + NULL, /* Tooltip */ + G_CALLBACK (on_menu_git_log) /* action callback */ + }, + { "ActionGitAdd", /* Action name */ GTK_STOCK_ADD, /* Stock icon, if any */ N_("_Add..."), /* Display label */ @@ -155,6 +164,8 @@ on_project_root_added (AnjutaPlugin *plugin, const gchar *name, "ActionMenuGit"); gtk_action_set_sensitive (git_menu_action, TRUE); + gtk_widget_set_sensitive (git_plugin->log_viewer, TRUE); + git_log_set_whole_project_sensitive (git_plugin, TRUE); g_free (project_root_uri); } @@ -177,6 +188,8 @@ on_project_root_removed (AnjutaPlugin *plugin, const gchar *name, "ActionMenuGit"); gtk_action_set_sensitive (git_menu_action, FALSE); + gtk_widget_set_sensitive (git_plugin->log_viewer, FALSE); + git_log_window_clear (git_plugin); } static void @@ -242,6 +255,17 @@ git_activate_plugin (AnjutaPlugin *plugin) on_editor_removed, NULL); + /* Log viewer */ + git_plugin->log_viewer = git_log_window_create (git_plugin); + anjuta_shell_add_widget (plugin->shell, + git_plugin->log_viewer, + "GitLogViewer", + _("Git Log"), + GTK_STOCK_ZOOM_100, + ANJUTA_SHELL_PLACEMENT_CENTER, + NULL); + g_object_unref (git_plugin->log_viewer); + /* Git needs a working directory to work with; it can't take full paths, * so make sure that Git can't be used if there's no project opened. */ git_menu_action = anjuta_ui_get_action (anjuta_shell_get_ui (plugin->shell, @@ -250,7 +274,10 @@ git_activate_plugin (AnjutaPlugin *plugin) "ActionMenuGit"); if (!git_plugin->project_root_directory) + { gtk_action_set_sensitive (git_menu_action, FALSE); + gtk_widget_set_sensitive (git_plugin->log_viewer, FALSE); + } return TRUE; } @@ -270,6 +297,8 @@ git_deactivate_plugin (AnjutaPlugin *plugin) anjuta_plugin_remove_watch (plugin, git_plugin->editor_watch_id, TRUE); + anjuta_shell_remove_widget (plugin->shell, git_plugin->log_viewer, NULL); + return TRUE; } diff --git a/plugins/git/plugin.h b/plugins/git/plugin.h index 69ca387b..62d3eb3c 100644 --- a/plugins/git/plugin.h +++ b/plugins/git/plugin.h @@ -55,6 +55,8 @@ struct _Git /* Watches */ gint project_root_watch_id; gint editor_watch_id; + + GtkWidget *log_viewer; }; struct _GitClass -- 2.11.4.GIT