From c28327bdf772ae81b5724efcd3a7b0417202ffc4 Mon Sep 17 00:00:00 2001 From: Federico Mena Quintero Date: Wed, 29 Mar 2006 00:59:08 +0000 Subject: New files with a simple framework for saving/loading settings from the 2006-03-28 Federico Mena Quintero * gtk/gtkfilechoosersettings.[ch]: New files with a simple framework for saving/loading settings from the file chooser in $XDG_CONFIG_HOME/gtk-2.0/gtkfilechooser. * gtk/gtkfilechooserdefault.c (gtk_file_chooser_default_unmap): Save the current settings. (settings_save): New helper function. We save the location_mode and show_hidden flags. (gtk_file_chooser_default_map): Load the settings. (settings_load): New helper function. * gtk/gtkfilechooserentry.c (_gtk_file_chooser_entry_set_file_part): Oops, don't modify in_change. Our handlers are what set the file_part, so they *must* be run when we modify the text. --- ChangeLog | 18 ++ ChangeLog.pre-2-10 | 18 ++ gtk/Makefile.am | 2 + gtk/gtkfilechooserdefault.c | 47 +++- gtk/gtkfilechooserentry.c | 2 - gtk/gtkfilechoosersettings.c | 535 +++++++++++++++++++++++++++++++++++++++++++ gtk/gtkfilechoosersettings.h | 75 ++++++ 7 files changed, 689 insertions(+), 8 deletions(-) create mode 100644 gtk/gtkfilechoosersettings.c create mode 100644 gtk/gtkfilechoosersettings.h diff --git a/ChangeLog b/ChangeLog index be75616fe1..c0c42adb6b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,21 @@ +2006-03-28 Federico Mena Quintero + + * gtk/gtkfilechoosersettings.[ch]: New files with a simple + framework for saving/loading settings from the file chooser in + $XDG_CONFIG_HOME/gtk-2.0/gtkfilechooser. + + * gtk/gtkfilechooserdefault.c (gtk_file_chooser_default_unmap): + Save the current settings. + (settings_save): New helper function. We save the location_mode + and show_hidden flags. + (gtk_file_chooser_default_map): Load the settings. + (settings_load): New helper function. + + * gtk/gtkfilechooserentry.c + (_gtk_file_chooser_entry_set_file_part): Oops, don't modify + in_change. Our handlers are what set the file_part, so they + *must* be run when we modify the text. + 2006-03-27 Federico Mena Quintero * gtk/gtkfilechooserprivate.h (struct _GtkFileChooserDefault): diff --git a/ChangeLog.pre-2-10 b/ChangeLog.pre-2-10 index be75616fe1..c0c42adb6b 100644 --- a/ChangeLog.pre-2-10 +++ b/ChangeLog.pre-2-10 @@ -1,3 +1,21 @@ +2006-03-28 Federico Mena Quintero + + * gtk/gtkfilechoosersettings.[ch]: New files with a simple + framework for saving/loading settings from the file chooser in + $XDG_CONFIG_HOME/gtk-2.0/gtkfilechooser. + + * gtk/gtkfilechooserdefault.c (gtk_file_chooser_default_unmap): + Save the current settings. + (settings_save): New helper function. We save the location_mode + and show_hidden flags. + (gtk_file_chooser_default_map): Load the settings. + (settings_load): New helper function. + + * gtk/gtkfilechooserentry.c + (_gtk_file_chooser_entry_set_file_part): Oops, don't modify + in_change. Our handlers are what set the file_part, so they + *must* be run when we modify the text. + 2006-03-27 Federico Mena Quintero * gtk/gtkfilechooserprivate.h (struct _GtkFileChooserDefault): diff --git a/gtk/Makefile.am b/gtk/Makefile.am index fe60c73d73..3ebbfcb8fa 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -302,6 +302,7 @@ gtk_private_h_sources = \ gtkfilechooserentry.h \ gtkfilechooserdefault.h \ gtkfilechooserprivate.h \ + gtkfilechoosersettings.h \ gtkfilechooserutils.h \ gtkfilesystemunix.h \ gtkfilesystemmodel.h \ @@ -382,6 +383,7 @@ gtk_c_sources = \ gtkfilechooserembed.c \ gtkfilechooserentry.c \ gtkfilechooserdefault.c \ + gtkfilechoosersettings.c \ gtkfilechooserutils.c \ gtkfilechooserwidget.c \ gtkfilefilter.c \ diff --git a/gtk/gtkfilechooserdefault.c b/gtk/gtkfilechooserdefault.c index 68a449a019..4855b10813 100644 --- a/gtk/gtkfilechooserdefault.c +++ b/gtk/gtkfilechooserdefault.c @@ -36,6 +36,7 @@ #include "gtkfilechooserdefault.h" #include "gtkfilechooserembed.h" #include "gtkfilechooserentry.h" +#include "gtkfilechoosersettings.h" #include "gtkfilechooserutils.h" #include "gtkfilechooser.h" #include "gtkfilesystemmodel.h" @@ -691,7 +692,7 @@ gtk_file_chooser_default_init (GtkFileChooserDefault *impl) impl->load_state = LOAD_EMPTY; impl->reload_state = RELOAD_EMPTY; impl->pending_select_paths = NULL; - impl->location_mode = LOCATION_MODE_FILENAME_ENTRY; + impl->location_mode = LOCATION_MODE_PATH_BAR; gtk_box_set_spacing (GTK_BOX (impl), 12); @@ -4984,6 +4985,40 @@ get_is_file_filtered (GtkFileChooserDefault *impl, return !result; } +static void +settings_load (GtkFileChooserDefault *impl) +{ + GtkFileChooserSettings *settings; + LocationMode location_mode; + gboolean show_hidden; + + settings = _gtk_file_chooser_settings_new (); + + location_mode = _gtk_file_chooser_settings_get_location_mode (settings); + show_hidden = _gtk_file_chooser_settings_get_show_hidden (settings); + + g_object_unref (settings); + + location_mode_set (impl, location_mode, TRUE); + gtk_file_chooser_set_show_hidden (GTK_FILE_CHOOSER (impl), show_hidden); +} + +static void +settings_save (GtkFileChooserDefault *impl) +{ + GtkFileChooserSettings *settings; + + settings = _gtk_file_chooser_settings_new (); + + _gtk_file_chooser_settings_set_location_mode (settings, impl->location_mode); + _gtk_file_chooser_settings_set_show_hidden (settings, gtk_file_chooser_get_show_hidden (GTK_FILE_CHOOSER (impl))); + + /* NULL GError */ + _gtk_file_chooser_settings_save (settings, NULL); + + g_object_unref (settings); +} + /* GtkWidget::map method */ static void gtk_file_chooser_default_map (GtkWidget *widget) @@ -5024,6 +5059,8 @@ gtk_file_chooser_default_map (GtkWidget *widget) bookmarks_changed_cb (impl->file_system, impl); + settings_load (impl); + profile_end ("end", NULL); } @@ -5035,6 +5072,8 @@ gtk_file_chooser_default_unmap (GtkWidget *widget) impl = GTK_FILE_CHOOSER_DEFAULT (widget); + settings_save (impl); + GTK_WIDGET_CLASS (parent_class)->unmap (widget); impl->reload_state = RELOAD_WAS_UNMAPPED; @@ -5996,11 +6035,7 @@ gtk_file_chooser_default_get_paths (GtkFileChooser *chooser) info.result = NULL; info.path_from_entry = NULL; - if (impl->action == GTK_FILE_CHOOSER_ACTION_SAVE - || impl->action == GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER - || ((impl->action == GTK_FILE_CHOOSER_ACTION_OPEN - || impl->action == GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER) - && impl->location_mode == LOCATION_MODE_FILENAME_ENTRY)) + if (impl->location_entry) { gboolean is_well_formed, is_empty, is_file_part_empty; diff --git a/gtk/gtkfilechooserentry.c b/gtk/gtkfilechooserentry.c index 2abf9af5f1..acbd1206c2 100644 --- a/gtk/gtkfilechooserentry.c +++ b/gtk/gtkfilechooserentry.c @@ -982,9 +982,7 @@ _gtk_file_chooser_entry_set_file_part (GtkFileChooserEntry *chooser_entry, { g_return_if_fail (GTK_IS_FILE_CHOOSER_ENTRY (chooser_entry)); - chooser_entry->in_change = TRUE; gtk_entry_set_text (GTK_ENTRY (chooser_entry), file_part); - chooser_entry->in_change = FALSE; } diff --git a/gtk/gtkfilechoosersettings.c b/gtk/gtkfilechoosersettings.c new file mode 100644 index 0000000000..861ca3f43f --- /dev/null +++ b/gtk/gtkfilechoosersettings.c @@ -0,0 +1,535 @@ +/* GTK - The GIMP Toolkit + * gtkfilechoosersettings.c: Internal settings for the GtkFileChooser widget + * Copyright (C) 2006, Novell, Inc. + * + * Authors: Federico Mena-Quintero + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/* TODO: + * + * - Persist these: + * - hpaned position + * - browse_for_other_folders? + * + * - Do we want lockdown? + */ + +#include +#include +#include +#include +#include "gtkfilechoosersettings.h" + +/* Increment this every time you change the configuration format */ +#define CONFIG_VERSION 0 + +#define ELEMENT_TOPLEVEL "gtkfilechooser" +#define ELEMENT_LOCATION "location" +#define ELEMENT_SHOW_HIDDEN "show_hidden" +#define ATTRIBUTE_VERSION "version" +#define ATTRIBUTE_MODE "mode" +#define ATTRIBUTE_VALUE "value" +#define MODE_PATH_BAR "path-bar" +#define MODE_FILENAME_ENTRY "filename-entry" +#define VALUE_TRUE "true" +#define VALUE_FALSE "false" + +#define EQ(a, b) (g_ascii_strcasecmp ((a), (b)) == 0) + +static char * +get_config_dirname (void) +{ + return g_build_filename (g_get_user_config_dir (), "gtk-2.0", NULL); +} + +static char * +get_config_filename (void) +{ + return g_build_filename (g_get_user_config_dir (), "gtk-2.0", "gtkfilechooser", NULL); +} + +static void +set_defaults (GtkFileChooserSettings *settings) +{ + settings->location_mode = LOCATION_MODE_PATH_BAR; + settings->show_hidden = FALSE; +} + +typedef enum { + STATE_START, + STATE_END, + STATE_ERROR, + STATE_IN_TOPLEVEL, + STATE_IN_LOCATION, + STATE_IN_SHOW_HIDDEN +} State; + +struct parse_state { + GtkFileChooserSettings *settings; + int version; + State state; +}; + +static const char * +get_attribute_value (const char **attribute_names, + const char **attribute_values, + const char *attribute) +{ + const char **name; + const char **value; + + name = attribute_names; + value = attribute_values; + + while (*name) + { + if (EQ (*name, attribute)) + return *value; + + name++; + value++; + } + + return NULL; +} + +static void +set_missing_attribute_error (struct parse_state *state, + int line, + int col, + const char *attribute, + GError **error) +{ + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + _("Line %d, column %d: missing attribute \"%s\""), + line, + col, + attribute); +} + +static void +set_unexpected_element_error (struct parse_state *state, + int line, + int col, + const char *element, + GError **error) +{ + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("Line %d, column %d: unexpected element \"%s\""), + line, + col, + element); +} + +static void +set_unexpected_element_end_error (struct parse_state *state, + int line, + int col, + const char *expected_element, + const char *unexpected_element, + GError **error) +{ + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("Line %d, column %d: expected end of element \"%s\", but got element for \"%s\" instead"), + line, + col, + expected_element, + unexpected_element); +} + + +static void +parse_start_element_cb (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + gpointer data, + GError **error) +{ + struct parse_state *state; + int line, col; + + state = data; + g_markup_parse_context_get_position (context, &line, &col); + + switch (state->state) + { + case STATE_START: + if (EQ (element_name, ELEMENT_TOPLEVEL)) + { + const char *version_str; + + state->state = STATE_IN_TOPLEVEL; + + version_str = get_attribute_value (attribute_names, attribute_values, ATTRIBUTE_VERSION); + if (!version_str) + state->version = -1; + else + if (sscanf (version_str, "%d", &state->version) != 1 || state->version < 0) + state->version = -1; + } + else + { + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("Line %d, column %d: expected \"%s\" at the toplevel, but found \"%s\" instead"), + line, + col, + ELEMENT_TOPLEVEL, + element_name); + } + break; + + case STATE_END: + g_assert_not_reached (); + break; + + case STATE_ERROR: + g_assert_not_reached (); + break; + + case STATE_IN_TOPLEVEL: + if (EQ (element_name, ELEMENT_LOCATION)) + { + const char *location_mode_str; + + state->state = STATE_IN_LOCATION; + + location_mode_str = get_attribute_value (attribute_names, attribute_values, ATTRIBUTE_MODE); + if (!location_mode_str) + set_missing_attribute_error (state, line, col, ATTRIBUTE_MODE, error); + else if (EQ (location_mode_str, MODE_PATH_BAR)) + state->settings->location_mode = LOCATION_MODE_PATH_BAR; + else if (EQ (location_mode_str, MODE_FILENAME_ENTRY)) + state->settings->location_mode = LOCATION_MODE_FILENAME_ENTRY; + else + { + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + _("Line %d, column %d: expected \"%s\" or \"%s\", but found \"%s\" instead"), + line, + col, + MODE_PATH_BAR, + MODE_FILENAME_ENTRY, + location_mode_str); + } + } + else if (EQ (element_name, ELEMENT_SHOW_HIDDEN)) + { + const char *value_str; + + state->state = STATE_IN_SHOW_HIDDEN; + + value_str = get_attribute_value (attribute_names, attribute_values, ATTRIBUTE_VALUE); + + if (!value_str) + set_missing_attribute_error (state, line, col, ATTRIBUTE_VALUE, error); + else if (EQ (value_str, VALUE_TRUE)) + state->settings->show_hidden = TRUE; + else if (EQ (value_str, VALUE_FALSE)) + state->settings->show_hidden = FALSE; + else + { + state->state = STATE_ERROR; + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_INVALID_CONTENT, + _("Line %d, column %d: expected \"%s\" or \"%s\", but found \"%s\" instead"), + line, + col, + VALUE_FALSE, + VALUE_TRUE, + value_str); + } + } + else + set_unexpected_element_error (state, line, col, element_name, error); + + break; + + case STATE_IN_LOCATION: + case STATE_IN_SHOW_HIDDEN: + set_unexpected_element_error (state, line, col, element_name, error); + break; + + default: + g_assert_not_reached (); + } +} + +static void +parse_end_element_cb (GMarkupParseContext *context, + const char *element_name, + gpointer data, + GError **error) +{ + struct parse_state *state; + int line, col; + + state = data; + g_markup_parse_context_get_position (context, &line, &col); + + switch (state->state) + { + case STATE_START: + g_assert_not_reached (); + break; + + case STATE_END: + g_assert_not_reached (); + break; + + case STATE_ERROR: + g_assert_not_reached (); + break; + + case STATE_IN_TOPLEVEL: + if (EQ (element_name, ELEMENT_TOPLEVEL)) + state->state = STATE_END; + else + set_unexpected_element_end_error (state, line, col, ELEMENT_TOPLEVEL, element_name, error); + + break; + + case STATE_IN_LOCATION: + if (EQ (element_name, ELEMENT_LOCATION)) + state->state = STATE_IN_TOPLEVEL; + else + set_unexpected_element_end_error (state, line, col, ELEMENT_LOCATION, element_name, error); + + break; + + case STATE_IN_SHOW_HIDDEN: + if (EQ (element_name, ELEMENT_SHOW_HIDDEN)) + state->state = STATE_IN_TOPLEVEL; + else + set_unexpected_element_end_error (state, line, col, ELEMENT_SHOW_HIDDEN, element_name, error); + + break; + + default: + g_assert_not_reached (); + } +} + +static gboolean +parse_config (GtkFileChooserSettings *settings, + const char *contents, + GError **error) +{ + GMarkupParser parser = { 0, }; + GMarkupParseContext *context; + struct parse_state state; + gboolean retval; + + parser.start_element = parse_start_element_cb; + parser.end_element = parse_end_element_cb; + + state.settings = settings; + state.version = -1; + state.state = STATE_START; + + context = g_markup_parse_context_new (&parser, + 0, + &state, + NULL); + + retval = g_markup_parse_context_parse (context, contents, -1, error); + g_markup_parse_context_free (context); + + return retval; +} + +static gboolean +read_config (GtkFileChooserSettings *settings, + GError **error) +{ + char *filename; + char *contents; + gsize contents_len; + gboolean success; + + filename = get_config_filename (); + + success = g_file_get_contents (filename, &contents, &contents_len, error); + g_free (filename); + + if (!success) + { + set_defaults (settings); + return FALSE; + } + + success = parse_config (settings, contents, error); + + g_free (contents); + + return success; +} + +static void +ensure_settings_read (GtkFileChooserSettings *settings) +{ + if (settings->settings_read) + return; + + /* NULL GError */ + read_config (settings, NULL); + + settings->settings_read = TRUE; +} + +G_DEFINE_TYPE (GtkFileChooserSettings, + _gtk_file_chooser_settings, + G_TYPE_OBJECT); + +static void +_gtk_file_chooser_settings_class_init (GtkFileChooserSettingsClass *class) +{ +} + +static void +_gtk_file_chooser_settings_init (GtkFileChooserSettings *settings) +{ +} + +GtkFileChooserSettings * +_gtk_file_chooser_settings_new (void) +{ + return g_object_new (GTK_FILE_CHOOSER_SETTINGS_TYPE, NULL); +} + +LocationMode +_gtk_file_chooser_settings_get_location_mode (GtkFileChooserSettings *settings) +{ + ensure_settings_read (settings); + return settings->location_mode; +} + +void +_gtk_file_chooser_settings_set_location_mode (GtkFileChooserSettings *settings, + LocationMode location_mode) +{ + settings->location_mode = location_mode; +} + +gboolean +_gtk_file_chooser_settings_get_show_hidden (GtkFileChooserSettings *settings) +{ + ensure_settings_read (settings); + return settings->show_hidden; +} + +void +_gtk_file_chooser_settings_set_show_hidden (GtkFileChooserSettings *settings, + gboolean show_hidden) +{ + settings->show_hidden = show_hidden ? TRUE : FALSE; +} + +static char * +settings_to_markup (GtkFileChooserSettings *settings) +{ + const char *location_mode_str; + const char *show_hidden_str; + + if (settings->location_mode == LOCATION_MODE_PATH_BAR) + location_mode_str = MODE_PATH_BAR; + else if (settings->location_mode == LOCATION_MODE_FILENAME_ENTRY) + location_mode_str = MODE_FILENAME_ENTRY; + else + { + g_assert_not_reached (); + return NULL; + } + + show_hidden_str = settings->show_hidden ? VALUE_TRUE : VALUE_FALSE; + + return g_strdup_printf + ("<" ELEMENT_TOPLEVEL ">\n" /* */ + " <" ELEMENT_LOCATION " " ATTRIBUTE_MODE "=\"%s\"/>\n" /* */ + " <" ELEMENT_SHOW_HIDDEN " " ATTRIBUTE_VALUE "=\"%s\"/>\n" /* */ + "\n", /* */ + location_mode_str, + show_hidden_str); +} + +gboolean +_gtk_file_chooser_settings_save (GtkFileChooserSettings *settings, + GError **error) +{ + char *contents; + char *filename; + char *dirname; + gboolean retval; + + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + contents = settings_to_markup (settings); + + filename = get_config_filename (); + dirname = NULL; + + retval = FALSE; + + if (!g_file_set_contents (filename, contents, -1, NULL)) + { + char *dirname; + int saved_errno; + + /* Directory is not there? */ + + dirname = get_config_dirname (); + if (g_mkdir_with_parents (dirname, 0700) != 0) /* 0700 per the XDG basedir spec */ + { + saved_errno = errno; + g_set_error (error, + G_FILE_ERROR, + g_file_error_from_errno (saved_errno), + _("Could not create directory: %s"), + dirname); + goto out; + } + + if (!g_file_set_contents (filename, contents, -1, error)) + goto out; + } + + retval = TRUE; + + out: + + g_free (contents); + g_free (dirname); + g_free (filename); + + return retval; +} diff --git a/gtk/gtkfilechoosersettings.h b/gtk/gtkfilechoosersettings.h new file mode 100644 index 0000000000..47f6abba68 --- /dev/null +++ b/gtk/gtkfilechoosersettings.h @@ -0,0 +1,75 @@ +/* GTK - The GIMP Toolkit + * gtkfilechoosersettings.h: Internal settings for the GtkFileChooser widget + * Copyright (C) 2006, Novell, Inc. + * + * Authors: Federico Mena-Quintero + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GTK_FILE_CHOOSER_SETTINGS_H__ +#define __GTK_FILE_CHOOSER_SETTINGS_H__ + +#include +#include "gtkfilechooserprivate.h" + +G_BEGIN_DECLS + +#define GTK_FILE_CHOOSER_SETTINGS_TYPE (_gtk_file_chooser_settings_get_type ()) + +typedef struct _GtkFileChooserSettings GtkFileChooserSettings; +typedef struct _GtkFileChooserSettingsClass GtkFileChooserSettingsClass; + +struct _GtkFileChooserSettings +{ + GObject object; + + LocationMode location_mode; + + guint settings_read : 1; + + guint show_hidden : 1; +}; + +struct _GtkFileChooserSettingsClass +{ + GObjectClass parent_class; +}; + +GType _gtk_file_chooser_settings_get_type (void) G_GNUC_CONST; + +GtkFileChooserSettings *_gtk_file_chooser_settings_new (void); + +LocationMode _gtk_file_chooser_settings_get_location_mode (GtkFileChooserSettings *settings); +void _gtk_file_chooser_settings_set_location_mode (GtkFileChooserSettings *settings, + LocationMode location_mode); + +gboolean _gtk_file_chooser_settings_get_show_hidden (GtkFileChooserSettings *settings); +void _gtk_file_chooser_settings_set_show_hidden (GtkFileChooserSettings *settings, + gboolean show_hidden); + +gboolean _gtk_file_chooser_settings_save (GtkFileChooserSettings *settings, + GError **error); + +/* FIXME: persist these options: + * + * - paned width + * - show_hidden + */ + +G_END_DECLS + +#endif -- cgit v1.2.1