[gitg/wip/submodules: 6/8] Implement enumeration of submodules in commit view



commit 0c3b69cb305907b8ce08ce59a57865acc1aedc39
Author: Jesse van den Kieboom <jessevdk gmail com>
Date:   Sat Dec 13 15:41:40 2014 +0100

    Implement enumeration of submodules in commit view

 gitg/Makefile.am                                   |    2 +
 gitg/commit/gitg-commit-paned.vala                 |    8 +
 gitg/commit/gitg-commit-sidebar.vala               |   90 ++--
 gitg/commit/gitg-commit-submodule-diff-view.vala   |   51 ++
 gitg/commit/gitg-commit-submodule-info.vala        |   97 ++++
 gitg/commit/gitg-commit.vala                       |  532 +++++++++++++-------
 gitg/resources/gitg-resources.xml                  |    2 +
 gitg/resources/ui/gitg-commit-paned.ui             |   18 +-
 .../ui/gitg-commit-submodule-diff-view.ui          |   96 ++++
 gitg/resources/ui/gitg-commit-submodule-info.ui    |  103 ++++
 gitg/resources/ui/style.css                        |   14 +
 libgitg/gitg-stage-status-enumerator.vala          |  234 ++++++++--
 libgitg/gitg-stage.vala                            |   43 +-
 tests/libgitg/test-stage.vala                      |   10 +-
 14 files changed, 1002 insertions(+), 298 deletions(-)
---
diff --git a/gitg/Makefile.am b/gitg/Makefile.am
index 61bba1b..9014446 100644
--- a/gitg/Makefile.am
+++ b/gitg/Makefile.am
@@ -78,6 +78,8 @@ gitg_gitg_VALASOURCES =                                               \
        gitg/commit/gitg-commit.vala                            \
        gitg/commit/gitg-commit-paned.vala                      \
        gitg/commit/gitg-commit-sidebar.vala                    \
+       gitg/commit/gitg-commit-submodule-diff-view.vala        \
+       gitg/commit/gitg-commit-submodule-info.vala             \
        gitg/commit/gitg-commit-dialog.vala                     \
        libgitg/libgitg-1.0.vapi                                \
        libgitg-ext/libgitg-ext-1.0.vapi
diff --git a/gitg/commit/gitg-commit-paned.vala b/gitg/commit/gitg-commit-paned.vala
index fcb760d..10f8bad 100644
--- a/gitg/commit/gitg-commit-paned.vala
+++ b/gitg/commit/gitg-commit-paned.vala
@@ -29,6 +29,9 @@ class Paned : Gtk.Paned
        [GtkChild (name = "diff_view")]
        private Gitg.DiffView d_diff_view;
 
+       [GtkChild (name = "submodule_diff_view")]
+       private SubmoduleDiffView d_submodule_diff_view;
+
        [GtkChild (name = "check_button_skip_hooks")]
        private Gtk.CheckButton d_check_button_skip_hooks;
 
@@ -51,6 +54,11 @@ class Paned : Gtk.Paned
                get { return d_diff_view; }
        }
 
+       public SubmoduleDiffView submodule_diff_view
+       {
+               get { return d_submodule_diff_view; }
+       }
+
        public bool skip_hooks
        {
                get { return d_check_button_skip_hooks.active; }
diff --git a/gitg/commit/gitg-commit-sidebar.vala b/gitg/commit/gitg-commit-sidebar.vala
index 517dbda..9dac9b0 100644
--- a/gitg/commit/gitg-commit-sidebar.vala
+++ b/gitg/commit/gitg-commit-sidebar.vala
@@ -33,33 +33,34 @@ class Sidebar : Gitg.Sidebar
 
        public signal void selected_items_changed(Gitg.SidebarItem[] items);
 
-       public class File : Object, Gitg.SidebarItem
+       public class Item : Object, Gitg.SidebarItem
        {
                public enum Type
                {
                        NONE,
                        STAGED,
                        UNSTAGED,
-                       UNTRACKED
+                       UNTRACKED,
+                       SUBMODULE
                }
 
-               Gitg.StageStatusFile d_file;
+               Gitg.StageStatusItem d_item;
                Type d_type;
 
-               public File(Gitg.StageStatusFile f, Type type)
+               public Item(Gitg.StageStatusItem item, Type type)
                {
-                       d_file = f;
+                       d_item = item;
                        d_type = type;
                }
 
-               public Gitg.StageStatusFile file
+               public Gitg.StageStatusItem item
                {
-                       get { return d_file; }
+                       get { return d_item; }
                }
 
                public string text
                {
-                       owned get { return d_file.path; }
+                       owned get { return d_item.path; }
                }
 
                public Type stage_type
@@ -67,33 +68,9 @@ class Sidebar : Gitg.Sidebar
                        get { return d_type; }
                }
 
-               private string? icon_for_status(Ggit.StatusFlags status)
-               {
-                       if ((status & (Ggit.StatusFlags.INDEX_NEW |
-                                          Ggit.StatusFlags.WORKING_TREE_NEW)) != 0)
-                       {
-                               return "list-add-symbolic";
-                       }
-                       else if ((status & (Ggit.StatusFlags.INDEX_MODIFIED |
-                                               Ggit.StatusFlags.INDEX_RENAMED |
-                                               Ggit.StatusFlags.INDEX_TYPECHANGE |
-                                               Ggit.StatusFlags.WORKING_TREE_MODIFIED |
-                                               Ggit.StatusFlags.WORKING_TREE_TYPECHANGE)) != 0)
-                       {
-                               return "text-editor-symbolic";
-                       }
-                       else if ((status & (Ggit.StatusFlags.INDEX_DELETED |
-                                               Ggit.StatusFlags.WORKING_TREE_DELETED)) != 0)
-                       {
-                               return "edit-delete-symbolic";
-                       }
-
-                       return null;
-               }
-
                public string? icon_name
                {
-                       owned get { return icon_for_status(d_file.flags); }
+                       owned get { return d_item.icon_name; }
                }
        }
 
@@ -123,38 +100,38 @@ class Sidebar : Gitg.Sidebar
                sel.mode = Gtk.SelectionMode.MULTIPLE;
        }
 
-       private File.Type get_item_type(Gitg.SidebarItem item)
+       private Item.Type get_item_type(Gitg.SidebarItem item)
        {
                var header = item as Gitg.SidebarStore.SidebarHeader;
 
                if (header != null)
                {
-                       return (File.Type)header.id;
+                       return (Item.Type)header.id;
                }
 
-               var file = item as File;
+               var sitem = item as Item;
 
-               if (file != null)
+               if (sitem != null)
                {
-                       return file.stage_type;
+                       return sitem.stage_type;
                }
 
-               return File.Type.NONE;
+               return Item.Type.NONE;
        }
 
-       private File.Type selected_type()
+       private Item.Type selected_type()
        {
                foreach (var item in get_selected_items<Gitg.SidebarItem>())
                {
                        var tp = get_item_type(item);
 
-                       if (tp != File.Type.NONE)
+                       if (tp != Item.Type.NONE)
                        {
                                return tp;
                        }
                }
 
-               return File.Type.NONE;
+               return Item.Type.NONE;
        }
 
        protected override bool select_function(Gtk.TreeSelection sel,
@@ -182,21 +159,32 @@ class Sidebar : Gitg.Sidebar
 
                var item = m.item_for_iter(iter);
 
-               // Prevent selection of the untracked header
+               // Prevent selection of the untracked and submodule headers
                var header = item as Gitg.SidebarStore.SidebarHeader;
 
-               if (header != null && (File.Type)header.id == File.Type.UNTRACKED)
+               if (header != null)
                {
-                       return false;
+                       var id = (Item.Type)header.id;
+
+                       if (id == Item.Type.UNTRACKED || id == Item.Type.SUBMODULE)
+                       {
+                               return false;
+                       }
                }
 
                var seltp = selected_type();
 
-               if (seltp == File.Type.NONE)
+               if (seltp == Item.Type.NONE)
                {
                        return true;
                }
 
+               // Do not allow multiple selections for submodules
+               if (seltp == Item.Type.SUBMODULE)
+               {
+                       return false;
+               }
+
                var tp = get_item_type(item);
                return tp == seltp;
        }
@@ -220,9 +208,9 @@ class Sidebar : Gitg.Sidebar
                }
        }
 
-       public File[] items_of_type(File.Type type)
+       public Item[] items_of_type(Item.Type type)
        {
-               var ret = new File[0];
+               var ret = new Item[0];
 
                model.foreach((m, path, iter) => {
                        var item = model.item_for_iter(iter);
@@ -232,11 +220,11 @@ class Sidebar : Gitg.Sidebar
                                return false;
                        }
 
-                       var file = item as File;
+                       var sitem = item as Item;
 
-                       if (file != null && file.stage_type == type)
+                       if (sitem != null && sitem.stage_type == type)
                        {
-                               ret += file;
+                               ret += sitem;
                        }
 
                        return false;
diff --git a/gitg/commit/gitg-commit-submodule-diff-view.vala 
b/gitg/commit/gitg-commit-submodule-diff-view.vala
new file mode 100644
index 0000000..1d2add9
--- /dev/null
+++ b/gitg/commit/gitg-commit-submodule-diff-view.vala
@@ -0,0 +1,51 @@
+/*
+ * This file is part of gitg
+ *
+ * Copyright (C) 2014 - Jesse van den Kieboom
+ *
+ * gitg 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.
+ *
+ * gitg 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 gitg. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace GitgCommit
+{
+
+[GtkTemplate (ui = "/org/gnome/gitg/ui/gitg-commit-submodule-diff-view.ui")]
+class SubmoduleDiffView : Gtk.Box
+{
+  [GtkChild (name = "info")]
+  private SubmoduleInfo d_info;
+
+  [GtkChild (name = "diff_view_staged")]
+  private Gitg.DiffView d_diff_view_staged;
+
+  [GtkChild (name = "diff_view_unstaged")]
+  private Gitg.DiffView d_diff_view_unstaged;
+
+  public SubmoduleInfo info
+  {
+    get { return d_info; }
+  }
+
+  public Gitg.DiffView diff_view_staged
+  {
+    get { return d_diff_view_staged; }
+  }
+
+  public Gitg.DiffView diff_view_unstaged
+  {
+    get { return d_diff_view_unstaged; }
+  }
+}
+
+}
\ No newline at end of file
diff --git a/gitg/commit/gitg-commit-submodule-info.vala b/gitg/commit/gitg-commit-submodule-info.vala
new file mode 100644
index 0000000..c9e6023
--- /dev/null
+++ b/gitg/commit/gitg-commit-submodule-info.vala
@@ -0,0 +1,97 @@
+/*
+ * This file is part of gitg
+ *
+ * Copyright (C) 2014 - Jesse van den Kieboom
+ *
+ * gitg 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.
+ *
+ * gitg 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 gitg. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace GitgCommit
+{
+
+[GtkTemplate (ui = "/org/gnome/gitg/ui/gitg-commit-submodule-info.ui")]
+class SubmoduleInfo : Gtk.Grid
+{
+       [GtkChild (name = "label_path")]
+       private Gtk.Label d_label_path;
+
+       [GtkChild (name = "label_url")]
+       private Gtk.Label d_label_url;
+
+       [GtkChild (name = "label_sha1")]
+       private Gtk.Label d_label_sha1;
+
+       [GtkChild (name = "label_subject")]
+       private Gtk.Label d_label_subject;
+
+       private Ggit.Submodule d_submodule;
+
+       public signal void request_open_repository(Ggit.Submodule submodule);
+
+       private void update_info_from_repository(Ggit.OId oid, Ggit.Submodule submodule)
+       {
+                       Gitg.Repository repo;
+
+                       d_label_subject.set_text("");
+
+                       try
+                       {
+                               repo = submodule.open() as Gitg.Repository;
+                       }
+                       catch (Error e)
+                       {
+                               return;
+                       }
+
+                       try
+                       {
+                               var commit = repo.lookup<Gitg.Commit>(oid);
+
+                               if (commit != null)
+                               {
+                                       d_label_subject.set_text(commit.get_subject());
+                               }
+                       }
+                       catch (Error e)
+                       {
+                       }
+       }
+
+       public Ggit.Submodule? submodule
+       {
+               set
+               {
+                       d_submodule = value;
+
+                       if (value != null)
+                       {
+                               d_label_path.set_text(value.get_path());
+                               d_label_url.set_text(value.get_url());
+
+                               var oid = value.get_workdir_id();
+                               d_label_sha1.set_text(oid.to_string());
+
+                               update_info_from_repository(oid, value);
+                       }
+               }
+       }
+
+       [GtkCallback]
+       private void on_open_button_clicked()
+       {
+               request_open_repository(d_submodule);
+       }
+}
+
+}
\ No newline at end of file
diff --git a/gitg/commit/gitg-commit.vala b/gitg/commit/gitg-commit.vala
index 841bd7d..5226729 100644
--- a/gitg/commit/gitg-commit.vala
+++ b/gitg/commit/gitg-commit.vala
@@ -87,183 +87,266 @@ namespace GitgCommit
                        return action == "commit";
                }
 
-               private delegate void StageUnstageCallback(Sidebar.File f);
+               private delegate void StageUnstageCallback(Sidebar.Item item);
 
                private delegate void UpdateDiffCallback();
                private UpdateDiffCallback? d_update_diff_callback;
 
-               private void show_unstaged_diff(Gitg.StageStatusFile[] files)
+               private void show_unstaged_diff(Gitg.StageStatusItem[] items)
                {
-                       var stage = application.repository.stage;
+                       show_submodule_ui(false);
+                       show_unstaged_diff_intern(application.repository, d_main.diff_view, items, true);
+               }
+
+               private void show_unstaged_diff_intern(Gitg.Repository         repository,
+                                                      Gitg.DiffView           view,
+                                                      Gitg.StageStatusItem[]? items,
+                                                      bool                    patchable)
+               {
+                       var stage = repository.stage;
 
-                       stage.diff_workdir_all.begin(files, d_main.diff_view.options, (obj, res) => {
+                       stage.diff_workdir_all.begin(items, view.options, (obj, res) => {
                                try
                                {
                                        var d = stage.diff_workdir_all.end(res);
 
-                                       d_main.diff_view.unstaged = true;
-                                       d_main.diff_view.staged = false;
+                                       view.unstaged = patchable;
+                                       view.staged = false;
 
                                        d_main.button_stage.label = _("_Stage selection");
+                                       d_main.button_stage.visible = patchable;
                                        d_main.button_discard.visible = true;
 
-                                       d_main.diff_view.diff = d;
+                                       view.diff = d;
                                }
                                catch
                                {
                                        // TODO: show error in diff
-                                       d_main.diff_view.diff = null;
+                                       view.diff = null;
                                }
                        });
 
                        d_update_diff_callback = () => {
-                               show_unstaged_diff(files);
+                               show_unstaged_diff_intern(repository, view, items, patchable);
                        };
                }
 
-               private async void stage_files(owned Gitg.StageStatusFile[] files)
+               private async void stage_items(owned Gitg.StageStatusItem[] items)
                {
                        var stage = application.repository.stage;
 
-                       foreach (var f in files)
+                       foreach (var item in items)
                        {
-                               if ((f.flags & Ggit.StatusFlags.WORKING_TREE_DELETED) != 0)
+                               var file = item as Gitg.StageStatusFile;
+
+                               if (file != null)
                                {
-                                       try
+                                       if ((file.flags & Ggit.StatusFlags.WORKING_TREE_DELETED) != 0)
                                        {
-                                               yield stage.delete_path(f.path);
+                                               try
+                                               {
+                                                       yield stage.delete_path(file.path);
+                                               }
+                                               catch (Error e)
+                                               {
+                                                       var msg = _("Failed to stage the removal of file 
`%s'").printf(file.path);
+                                                       application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
+
+                                                       break;
+                                               }
                                        }
-                                       catch (Error e)
+                                       else
                                        {
-                                               var msg = _("Failed to stage the removal of file 
`%s'").printf(f.path);
-                                               application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
+                                               try
+                                               {
+                                                       yield stage.stage_path(file.path);
+                                               }
+                                               catch (Error e)
+                                               {
+                                                       var msg = _("Failed to stage the file 
`%s'").printf(file.path);
+                                                       application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
 
-                                               break;
+                                                       break;
+                                               }
                                        }
                                }
                                else
                                {
-                                       try
-                                       {
-                                               yield stage.stage_path(f.path);
-                                       }
-                                       catch (Error e)
-                                       {
-                                               var msg = _("Failed to stage the file `%s'").printf(f.path);
-                                               application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
-
-                                               break;
-                                       }
+                                       // TODO: stage submodule item
                                }
                        }
 
                        reload();
                }
 
-               private void on_unstaged_activated(Gitg.StageStatusFile[] files)
+               private void show_submodule_ui(bool show)
+               {
+                       d_main.submodule_diff_view.set_visible(show);
+                       d_main.diff_view.set_visible(!show);
+
+                       if (show)
+                       {
+                               d_main.diff_view.diff = null;
+                       }
+                       else
+                       {
+                               var view = d_main.submodule_diff_view;
+
+                               view.info.submodule = null;
+                               view.diff_view_staged.diff = null;
+                               view.diff_view_unstaged.diff = null;
+                       }
+               }
+
+               private void on_unstaged_activated(Gitg.StageStatusItem[] items)
                {
-                       stage_files.begin(files, (obj, res) => {
-                               stage_files.end(res);
+                       stage_items.begin(items, (obj, res) => {
+                               stage_items.end(res);
                        });
                }
 
-               private void show_staged_diff(Gitg.StageStatusFile[] files)
+               private void show_submodule_diff(Gitg.StageStatusSubmodule sub)
                {
-                       var stage = application.repository.stage;
+                       show_submodule_ui(true);
+
+                       var view = d_main.submodule_diff_view;
+
+                       view.info.submodule = sub.submodule;
+
+                       Gitg.Repository repo;
+
+                       try
+                       {
+                               repo = sub.submodule.open() as Gitg.Repository;
+                       }
+                       catch (Error e)
+                       {
+                               view.diff_view_staged.diff = null;
+                               view.diff_view_unstaged.diff = null;
+
+                               return;
+                       }
+
+                       show_staged_diff_intern(repo, view.diff_view_staged, null, false);
+                       show_unstaged_diff_intern(repo, view.diff_view_unstaged, null, false);
+               }
 
-                       stage.diff_index_all.begin(files, d_main.diff_view.options, (obj, res) => {
+               private void show_staged_diff_intern(Gitg.Repository         repository,
+                                                    Gitg.DiffView           view,
+                                                    Gitg.StageStatusItem[]? items,
+                                                    bool                    patchable)
+               {
+                       var stage = repository.stage;
+
+                       stage.diff_index_all.begin(items, view.options, (obj, res) => {
                                try
                                {
                                        var d = stage.diff_index_all.end(res);
 
-                                       d_main.diff_view.unstaged = false;
-                                       d_main.diff_view.staged = true;
+                                       view.unstaged = false;
+                                       view.staged = patchable;
 
                                        d_main.button_stage.label = _("_Unstage selection");
+                                       d_main.button_stage.visible = patchable;
                                        d_main.button_discard.visible = false;
 
-                                       d_main.diff_view.diff = d;
+                                       view.diff = d;
                                }
                                catch
                                {
                                        // TODO: error reporting
-                                       d_main.diff_view.diff = null;
+                                       view.diff = null;
                                }
                        });
 
                        d_update_diff_callback = () => {
-                               show_staged_diff(files);
+                               show_staged_diff_intern(repository, view, items, patchable);
                        };
                }
 
-               private async void unstage_files(owned Gitg.StageStatusFile[] files)
+               private void show_staged_diff(Gitg.StageStatusItem[] items)
+               {
+                       show_submodule_ui(false);
+                       show_staged_diff_intern(application.repository, d_main.diff_view, items, true);
+               }
+
+               private async void unstage_items(owned Gitg.StageStatusItem[] items)
                {
                        var stage = application.repository.stage;
 
-                       foreach (var f in files)
+                       foreach (var item in items)
                        {
-                               if ((f.flags & Ggit.StatusFlags.INDEX_NEW) != 0)
+                               var file = item as Gitg.StageStatusFile;
+
+                               if (file != null)
                                {
-                                       try
+                                       if ((file.flags & Ggit.StatusFlags.INDEX_NEW) != 0)
                                        {
-                                               yield stage.delete_path(f.path);
+                                               try
+                                               {
+                                                       yield stage.delete_path(file.path);
+                                               }
+                                               catch (Error e)
+                                               {
+                                                       var msg = _("Failed to unstage the removal of file 
`%s'").printf(file.path);
+                                                       application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
+
+                                                       break;
+                                               }
                                        }
-                                       catch (Error e)
+                                       else
                                        {
-                                               var msg = _("Failed to unstage the removal of file 
`%s'").printf(f.path);
-                                               application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
+                                               try
+                                               {
+                                                       yield stage.unstage_path(file.path);
+                                               }
+                                               catch (Error e)
+                                               {
+                                                       var msg = _("Failed to unstage the file 
`%s'").printf(file.path);
+                                                       application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
 
-                                               break;
+                                                       break;
+                                               }
                                        }
                                }
                                else
                                {
-                                       try
-                                       {
-                                               yield stage.unstage_path(f.path);
-                                       }
-                                       catch (Error e)
-                                       {
-                                               var msg = _("Failed to unstage the file `%s'").printf(f.path);
-                                               application.show_infobar(msg, e.message, 
Gtk.MessageType.ERROR);
-
-                                               break;
-                                       }
+                                       // TODO: submodule?
                                }
                        }
 
                        reload();
                }
 
-               private void on_staged_activated(Gitg.StageStatusFile[] files)
+               private void on_staged_activated(Gitg.StageStatusItem[] items)
                {
-                       unstage_files.begin(files, (obj, res) => {
-                               unstage_files.end(res);
+                       unstage_items.begin(items, (obj, res) => {
+                               unstage_items.end(res);
                        });
                }
 
-               private Sidebar.File[] append_files(Gitg.SidebarStore      model,
-                                                   Gitg.StageStatusFile[] files,
-                                                   Sidebar.File.Type      type,
+               private Sidebar.Item[] append_items(Gitg.SidebarStore      model,
+                                                   Gitg.StageStatusItem[] items,
+                                                   Sidebar.Item.Type      type,
                                                    Gee.HashSet<string>?   selected_paths,
                                                    StageUnstageCallback?  callback)
                {
-                       var ret = new Sidebar.File[0];
+                       var ret = new Sidebar.Item[0];
 
-                       foreach (var f in files)
+                       foreach (var item in items)
                        {
-                               var item = new Sidebar.File(f, type);
+                               var sitem = new Sidebar.Item(item, type);
 
-                               if (selected_paths != null && selected_paths.contains(f.path))
+                               if (selected_paths != null && selected_paths.contains(item.path))
                                {
-                                       ret += item;
+                                       ret += sitem;
                                }
 
-                               item.activated.connect((numclick) => {
-                                       callback(item);
+                               sitem.activated.connect((numclick) => {
+                                       callback(sitem);
                                });
 
-                               model.append(item);
+                               model.append(sitem);
                        }
 
                        return ret;
@@ -283,17 +366,17 @@ namespace GitgCommit
                        var sb = d_main.sidebar;
                        var model = sb.model;
 
-                       Sidebar.File.Type selected_type;
-                       Gitg.StageStatusFile[] selected_files;
+                       Sidebar.Item.Type selected_type;
+                       Gitg.StageStatusItem[] selected_items;
 
-                       selected_files = files_for_items(sb.get_selected_items<Gitg.SidebarItem>(),
+                       selected_items = items_for_items(sb.get_selected_items<Gitg.SidebarItem>(),
                                                         out selected_type);
 
                        var selected_paths = new Gee.HashSet<string>();
 
-                       foreach (var f in selected_files)
+                       foreach (var item in selected_items)
                        {
-                               selected_paths.add(f.path);
+                               selected_paths.add(item.path);
                        }
 
                        // Preload author avatar
@@ -320,78 +403,87 @@ namespace GitgCommit
                        var options = new Ggit.StatusOptions(opts, show, null);
                        var enumerator = stage.file_status(options);
 
-                       var indexflags = Ggit.StatusFlags.INDEX_NEW |
-                                        Ggit.StatusFlags.INDEX_MODIFIED |
-                                        Ggit.StatusFlags.INDEX_DELETED |
-                                        Ggit.StatusFlags.INDEX_RENAMED |
-                                        Ggit.StatusFlags.INDEX_TYPECHANGE;
-
-                       var workflags = Ggit.StatusFlags.WORKING_TREE_MODIFIED |
-                                       Ggit.StatusFlags.WORKING_TREE_DELETED |
-                                       Ggit.StatusFlags.WORKING_TREE_TYPECHANGE;
-
-                       var untrackedflags = Ggit.StatusFlags.WORKING_TREE_NEW;
-
-                       enumerator.next_files.begin(-1, (obj, res) => {
-                               var files = enumerator.next_files.end(res);
+                       enumerator.next_items.begin(-1, (obj, res) => {
+                               var items = enumerator.next_items.end(res);
 
-                               var staged = new Gitg.StageStatusFile[files.length];
+                               var staged = new Gitg.StageStatusItem[items.length];
                                staged.length = 0;
 
-                               var unstaged = new Gitg.StageStatusFile[files.length];
+                               var unstaged = new Gitg.StageStatusItem[items.length];
                                unstaged.length = 0;
 
-                               var untracked = new Gitg.StageStatusFile[files.length];
+                               var untracked = new Gitg.StageStatusItem[items.length];
                                untracked.length = 0;
 
-                               foreach (var f in files)
+                               var dirty = new Gitg.StageStatusItem[items.length];
+                               dirty.length = 0;
+
+                               bool hassub = false;
+
+                               foreach (var item in items)
                                {
-                                       if ((f.flags & indexflags) != 0)
+                                       if (item.is_staged)
+                                       {
+                                               staged += item;
+                                       }
+
+                                       if (item.is_unstaged)
                                        {
-                                               staged += f;
+                                               unstaged += item;
                                        }
 
-                                       if ((f.flags & workflags) != 0)
+                                       if (item.is_untracked)
                                        {
-                                               unstaged += f;
+                                               untracked += item;
                                        }
 
-                                       if ((f.flags & untrackedflags) != 0)
+                                       var sub = item as Gitg.StageStatusSubmodule;
+
+                                       if (sub != null)
                                        {
-                                               untracked += f;
+                                               hassub = true;
+
+                                               if (sub.is_dirty)
+                                               {
+                                                       dirty += item;
+                                               }
                                        }
                                }
 
                                model.clear();
                                d_main.diff_view.diff = null;
 
-                               var staged_header = model.begin_header(_("Staged"), 
(uint)Sidebar.File.Type.STAGED);
+                               var current_staged = new Sidebar.Item[0];
+                               var current_unstaged = new Sidebar.Item[0];
+                               var current_untracked = new Sidebar.Item[0];
+                               var current_submodules = new Sidebar.Item[0];
+
+                               // Populate staged items
+                               var staged_header = model.begin_header(_("Staged"), 
(uint)Sidebar.Item.Type.STAGED);
 
                                staged_header.activated.connect((numclick) => {
                                        on_unstage_selected_items();
                                });
 
-                               var current_staged = new Sidebar.File[0];
-                               var current_unstaged = new Sidebar.File[0];
-
                                if (staged.length == 0)
                                {
                                        model.append_dummy(_("No staged files"));
                                }
                                else
                                {
-                                       current_staged = append_files(model,
+                                       current_staged = append_items(model,
                                                                      staged,
-                                                                     Sidebar.File.Type.STAGED,
+                                                                     Sidebar.Item.Type.STAGED,
                                                                      selected_paths,
-                                                                     (f) => {
-                                                                               on_staged_activated(new 
Gitg.StageStatusFile[] {f.file});
+                                                                     (item) => {
+                                                                        on_staged_activated(new 
Gitg.StageStatusItem[] {item.item});
                                                                      });
                                }
 
                                model.end_header();
 
-                               var unstaged_header = model.begin_header(_("Unstaged"), 
(uint)Sidebar.File.Type.UNSTAGED);
+                               // Populate unstaged items
+                               var unstaged_header = model.begin_header(_("Unstaged"), 
(uint)Sidebar.Item.Type.UNSTAGED);
 
                                unstaged_header.activated.connect((numclick) => {
                                        on_stage_selected_items();
@@ -403,18 +495,19 @@ namespace GitgCommit
                                }
                                else
                                {
-                                       current_unstaged = append_files(model,
+                                       current_unstaged = append_items(model,
                                                                        unstaged,
-                                                                       Sidebar.File.Type.UNSTAGED,
+                                                                       Sidebar.Item.Type.UNSTAGED,
                                                                        selected_paths,
-                                                                       (f) => {
-                                                                               on_unstaged_activated(new 
Gitg.StageStatusFile[] {f.file});
+                                                                       (item) => {
+                                                                               on_unstaged_activated(new 
Gitg.StageStatusItem[] {item.item});
                                                                        });
                                }
 
                                model.end_header();
 
-                               model.begin_header(_("Untracked"), (uint)Sidebar.File.Type.UNTRACKED);
+                               // Populate untracked items
+                               model.begin_header(_("Untracked"), (uint)Sidebar.Item.Type.UNTRACKED);
 
                                if (untracked.length == 0)
                                {
@@ -422,17 +515,31 @@ namespace GitgCommit
                                }
                                else
                                {
-                                       append_files(model,
-                                                    untracked,
-                                                    Sidebar.File.Type.UNTRACKED,
-                                                    null,
-                                                    (f) => {
-                                                        on_unstaged_activated(new Gitg.StageStatusFile[] 
{f.file});
-                                                    });
+                                       current_untracked = append_items(model,
+                                                                        untracked,
+                                                                        Sidebar.Item.Type.UNTRACKED,
+                                                                        selected_paths,
+                                                                        (item) => {
+                                                                                       
on_unstaged_activated(new Gitg.StageStatusItem[] {item.item});
+                                                                               });
                                }
 
                                model.end_header();
 
+                               // Populate submodule items
+                               if (hassub)
+                               {
+                                       model.begin_header(_("Submodule"), (uint)Sidebar.Item.Type.SUBMODULE);
+                                       current_submodules = append_items(model,
+                                                                         dirty,
+                                                                         Sidebar.Item.Type.SUBMODULE,
+                                                                         selected_paths,
+                                                                         (item) => {
+                                                                               on_unstaged_activated(new 
Gitg.StageStatusItem[] {item.item});
+                                                                         });
+                                       model.end_header();
+                               }
+
                                d_main.sidebar.expand_all();
                                d_has_staged = staged.length != 0;
 
@@ -440,25 +547,52 @@ namespace GitgCommit
 
                                if (selected_paths.size != 0)
                                {
-                                       Sidebar.File[] sel;
+                                       Sidebar.Item[] sel = null;
 
-                                       if (selected_type == Sidebar.File.Type.STAGED)
+                                       switch (selected_type)
                                        {
-                                               sel = (current_staged.length != 0) ? current_staged : 
current_unstaged;
+                                       case Sidebar.Item.Type.STAGED:
+                                               sel = current_staged;
+                                               break;
+                                       case Sidebar.Item.Type.UNSTAGED:
+                                               sel = current_unstaged;
+                                               break;
+                                       case Sidebar.Item.Type.UNTRACKED:
+                                               sel = current_untracked;
+                                               break;
+                                       case Sidebar.Item.Type.SUBMODULE:
+                                               sel = current_submodules;
+                                               break;
                                        }
-                                       else
+
+                                       if (sel == null || sel.length == 0)
+                                       {
+                                               sel = current_staged;
+                                       }
+
+                                       if (sel == null || sel.length == 0)
+                                       {
+                                               sel = current_unstaged;
+                                       }
+
+                                       if (sel == null || sel.length == 0)
+                                       {
+                                               sel = current_untracked;
+                                       }
+
+                                       if (sel == null || sel.length == 0)
                                        {
-                                               sel = (current_unstaged.length != 0) ? current_unstaged : 
current_staged;
+                                               sel = current_submodules;
                                        }
 
-                                       if (sel.length != 0)
+                                       if (sel != null && sel.length != 0)
                                        {
                                                foreach (var item in sel)
                                                {
                                                        d_main.sidebar.select(item);
                                                }
                                        }
-                                       else if (selected_type == Sidebar.File.Type.STAGED)
+                                       else if (selected_type == Sidebar.Item.Type.STAGED)
                                        {
                                                d_main.sidebar.select(staged_header);
                                        }
@@ -722,7 +856,7 @@ namespace GitgCommit
                                {
                                        secmsg = _("Your email is not configured yet. Please go to the user 
configuration and provide your email.");
                                }
-                       
+
                                // TODO: better to show user info dialog directly or something
                                application.show_infobar(_("Failed to pass pre-commit"),
                                                         secmsg,
@@ -872,15 +1006,15 @@ namespace GitgCommit
                        }
                }
 
-               private bool do_discard_files(GitgExt.UserQuery q, Gitg.StageStatusFile[] files)
+               private bool do_discard_items(GitgExt.UserQuery q, Gitg.StageStatusItem[] items)
                {
                        application.busy = true;
 
-                       var paths = new string[files.length];
+                       var paths = new string[items.length];
 
-                       for (var i = 0; i < files.length; i++)
+                       for (var i = 0; i < items.length; i++)
                        {
-                               paths[i] = files[i].path;
+                               paths[i] = items[i].path;
                        }
 
                        revert_paths.begin(paths, (o, ret) => {
@@ -904,25 +1038,25 @@ namespace GitgCommit
                        return false;
                }
 
-               private void on_discard_menu_activated(Gitg.StageStatusFile[] files)
+               private void on_discard_menu_activated(Gitg.StageStatusItem[] items)
                {
                        var primary = _("Discard changes");
                        string secondary;
 
-                       if (files.length == 1)
+                       if (items.length == 1)
                        {
-                               secondary = _("Are you sure you want to permanently discard all changes made 
to the file `%s'?").printf(files[0].path);
+                               secondary = _("Are you sure you want to permanently discard all changes made 
to the file `%s'?").printf(items[0].path);
                        }
                        else
                        {
-                               var paths = new string[files.length - 1];
+                               var paths = new string[items.length - 1];
 
-                               for (var i = 0; i < files.length - 1; i++)
+                               for (var i = 0; i < items.length - 1; i++)
                                {
-                                       paths[i] = @"`$(files[i].path)'";
+                                       paths[i] = @"`$(items[i].path)'";
                                }
 
-                               secondary = _("Are you sure you want to permanently discard all changes made 
to the files %s and `%s'?").printf(string.joinv(", ", paths), files[files.length - 1].path);
+                               secondary = _("Are you sure you want to permanently discard all changes made 
to the files %s and `%s'?").printf(string.joinv(", ", paths), items[items.length - 1].path);
                        }
 
                        var q = new GitgExt.UserQuery();
@@ -941,7 +1075,7 @@ namespace GitgCommit
                        q.response.connect((w, r) => {
                                if (r == Gtk.ResponseType.OK)
                                {
-                                       return do_discard_files(q, files);
+                                       return do_discard_items(q, items);
                                }
 
                                return true;
@@ -959,65 +1093,65 @@ namespace GitgCommit
                                return;
                        }
 
-                       Sidebar.File.Type type;
+                       Sidebar.Item.Type type;
 
-                       var files = files_for_items(items, out type);
+                       var sitems = items_for_items(items, out type);
 
-                       if (type == Sidebar.File.Type.UNSTAGED ||
-                           type == Sidebar.File.Type.UNTRACKED)
+                       if (type == Sidebar.Item.Type.UNSTAGED ||
+                           type == Sidebar.Item.Type.UNTRACKED)
                        {
                                var stage = new Gtk.MenuItem.with_mnemonic(_("_Stage changes"));
                                menu.append(stage);
 
                                stage.activate.connect(() => {
-                                       on_unstaged_activated(files);
+                                       on_unstaged_activated(sitems);
                                });
                        }
 
-                       if (type == Sidebar.File.Type.STAGED)
+                       if (type == Sidebar.Item.Type.STAGED)
                        {
                                var stage = new Gtk.MenuItem.with_mnemonic(_("_Unstage changes"));
                                menu.append(stage);
 
                                stage.activate.connect(() => {
-                                       on_staged_activated(files);
+                                       on_staged_activated(sitems);
                                });
                        }
 
-                       if (type == Sidebar.File.Type.UNSTAGED)
+                       if (type == Sidebar.Item.Type.UNSTAGED)
                        {
                                var discard = new Gtk.MenuItem.with_mnemonic(_("_Discard changes"));
                                menu.append(discard);
 
                                discard.activate.connect(() => {
-                                       on_discard_menu_activated(files);
+                                       on_discard_menu_activated(sitems);
                                });
                        }
                }
 
-               private Gitg.StageStatusFile[] files_to_stage_files(Sidebar.File[] files)
+               private Gitg.StageStatusItem[] items_to_stage_items(Sidebar.Item[] items)
                {
-                       var ret = new Gitg.StageStatusFile[files.length];
+                       var ret = new Gitg.StageStatusItem[items.length];
 
                        for (var i = 0; i < ret.length; i++)
                        {
-                               ret[i] = files[i].file;
+                               ret[i] = items[i].item;
                        }
 
                        return ret;
                }
 
-               private Gitg.StageStatusFile[] stage_status_files_of_type(Sidebar.File.Type type)
+               private Gitg.StageStatusItem[] stage_status_items_of_type(Sidebar.Item.Type type)
                {
-                       return files_to_stage_files(d_main.sidebar.items_of_type(type));
+                       return items_to_stage_items(d_main.sidebar.items_of_type(type));
                }
 
-               private Gitg.StageStatusFile[] files_for_items(Gitg.SidebarItem[] items, out 
Sidebar.File.Type type)
+               private Gitg.StageStatusItem[] items_for_items(Gitg.SidebarItem[] items, out 
Sidebar.Item.Type type)
                {
-                       var files = new Gitg.StageStatusFile[items.length];
-                       files.length = 0;
+                       var ret = new Gitg.StageStatusItem[items.length];
+                       ret.length = 0;
 
-                       type = Sidebar.File.Type.NONE;
+                       type = Sidebar.Item.Type.NONE;
 
                        foreach (var item in items)
                        {
@@ -1025,68 +1159,73 @@ namespace GitgCommit
 
                                if (header != null)
                                {
-                                       type = (Sidebar.File.Type)header.id;
-                                       return stage_status_files_of_type(type);
+                                       type = (Sidebar.Item.Type)header.id;
+                                       return stage_status_items_of_type(type);
                                }
 
-                               var file = item as Sidebar.File;
+                               var sitem = item as Sidebar.Item;
 
-                               if (file != null)
+                               if (sitem != null)
                                {
-                                       files += file.file;
-                                       type = file.stage_type;
+                                       ret += sitem.item;
+                                       type = sitem.stage_type;
                                }
                        }
 
-                       return files;
+                       return ret;
                }
 
                private void sidebar_selection_changed(Gitg.SidebarItem[] items)
                {
-                       Sidebar.File.Type type;
+                       Sidebar.Item.Type type;
 
-                       var files = files_for_items(items, out type);
+                       var sitems = items_for_items(items, out type);
 
-                       if (files.length == 0)
+                       if (sitems.length == 0)
                        {
+                               show_submodule_ui(false);
                                d_main.diff_view.diff = null;
                                return;
                        }
 
-                       if (type == Sidebar.File.Type.STAGED)
+                       if (type == Sidebar.Item.Type.SUBMODULE)
                        {
-                               show_staged_diff(files);
+                               show_submodule_diff((Gitg.StageStatusSubmodule)sitems[0]);
+                       }
+                       else if (type == Sidebar.Item.Type.STAGED)
+                       {
+                               show_staged_diff(sitems);
                        }
                        else
                        {
-                               show_unstaged_diff(files);
+                               show_unstaged_diff(sitems);
                        }
                }
 
                private void on_stage_selected_items()
                {
                        var sel = d_main.sidebar.get_selected_items<Gitg.SidebarItem>();
-                       Sidebar.File.Type type;
+                       Sidebar.Item.Type type;
 
-                       var files = files_for_items(sel, out type);
+                       var sitems = items_for_items(sel, out type);
 
-                       if (files.length != 0 && (type == Sidebar.File.Type.UNSTAGED ||
-                                                 type == Sidebar.File.Type.UNTRACKED))
+                       if (sitems.length != 0 && (type == Sidebar.Item.Type.UNSTAGED ||
+                                                  type == Sidebar.Item.Type.UNTRACKED))
                        {
-                               on_unstaged_activated(files);
+                               on_unstaged_activated(sitems);
                        }
                }
 
                private void on_unstage_selected_items()
                {
                        var sel = d_main.sidebar.get_selected_items<Gitg.SidebarItem>();
-                       Sidebar.File.Type type;
+                       Sidebar.Item.Type type;
 
-                       var files = files_for_items(sel, out type);
+                       var sitems = items_for_items(sel, out type);
 
-                       if (files.length != 0 && type == Sidebar.File.Type.STAGED)
+                       if (sitems.length != 0 && type == Sidebar.Item.Type.STAGED)
                        {
-                               on_staged_activated(files);
+                               on_staged_activated(sitems);
                        }
                }
 
@@ -1110,13 +1249,13 @@ namespace GitgCommit
 
                        d_main.sidebar.discard_selection.connect(() => {
                                var sel = d_main.sidebar.get_selected_items<Gitg.SidebarItem>();
-                               Sidebar.File.Type type;
+                               Sidebar.Item.Type type;
 
-                               var files = files_for_items(sel, out type);
+                               var sitems = items_for_items(sel, out type);
 
-                               if (files.length != 0 && type == Sidebar.File.Type.UNSTAGED)
+                               if (sitems.length != 0 && type == Sidebar.Item.Type.UNSTAGED)
                                {
-                                       on_discard_menu_activated(files);
+                                       on_discard_menu_activated(sitems);
                                }
                        });
 
@@ -1134,6 +1273,23 @@ namespace GitgCommit
                                on_discard_clicked();
                        });
 
+                       d_main.submodule_diff_view.info.request_open_repository.connect((submodule) => {
+                               try
+                               {
+                                       var app = application.open_new(submodule.open(), "commit");
+
+                                       ((Gtk.Window)app).delete_event.connect(() => {
+                                               reload();
+                                               return false;
+                                       });
+                               }
+                               catch (Error e)
+                               {
+                                       // TODO: show error message
+                                       stderr.printf("Failed to open submodule repository: %s\n", e.message);
+                               }
+                       });
+
                        d_main.sidebar.populate_popup.connect(do_populate_menu);
 
                        var settings = new Settings("org.gnome.gitg.preferences.commit.diff");
diff --git a/gitg/resources/gitg-resources.xml b/gitg/resources/gitg-resources.xml
index f9e831d..e04c14e 100644
--- a/gitg/resources/gitg-resources.xml
+++ b/gitg/resources/gitg-resources.xml
@@ -14,6 +14,8 @@
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-history-ref-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-history-ref-header.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-commit-paned.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">ui/gitg-commit-submodule-diff-view.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">ui/gitg-commit-submodule-info.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-commit-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-create-branch-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/gitg-create-tag-dialog.ui</file>
diff --git a/gitg/resources/ui/gitg-commit-paned.ui b/gitg/resources/ui/gitg-commit-paned.ui
index f8bd7eb..902bf2e 100644
--- a/gitg/resources/ui/gitg-commit-paned.ui
+++ b/gitg/resources/ui/gitg-commit-paned.ui
@@ -1,8 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <!-- interface-requires gtk+ 3.3 -->
-  <!-- interface-requires gitg 0.0 -->
-  <!-- interface-requires gd 1.0 -->
+  <!-- interface-requires gitg 3.0 -->
   <template class="GitgCommitPaned" parent="GtkPaned">
     <property name="visible">True</property>
     <property name="hexpand">True</property>
@@ -50,6 +49,21 @@
           </object>
         </child>
         <child>
+          <object class="GitgCommitSubmoduleDiffView" id="submodule_diff_view">
+            <property name="visible">False</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">12</property>
+            <property name="margin_right">12</property>
+            <property name="margin_top">12</property>
+            <property name="margin_bottom">12</property>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
           <object class="GtkFrame" id="frame_commit">
             <property name="visible">True</property>
             <style>
diff --git a/gitg/resources/ui/gitg-commit-submodule-diff-view.ui 
b/gitg/resources/ui/gitg-commit-submodule-diff-view.ui
new file mode 100644
index 0000000..b843dae
--- /dev/null
+++ b/gitg/resources/ui/gitg-commit-submodule-diff-view.ui
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.3"/>
+  <requires lib="gitg" version="3.0"/>
+  <template class="GitgCommitSubmoduleDiffView" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <property name="spacing">6</property>
+    <child>
+      <object class="GitgCommitSubmoduleInfo" id="info">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_staged">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="margin_top">12</property>
+        <property name="label" translatable="yes">Staged:</property>
+        <style>
+          <class name="title-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkFrame" id="frame_staged">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GitgDiffView" id="diff_view_staged">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_nstaged">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="margin_top">12</property>
+        <property name="label" translatable="yes">Unstaged:</property>
+        <style>
+          <class name="title-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkFrame" id="frame_unstaged">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="vexpand">True</property>
+        <child>
+          <object class="GitgDiffView" id="diff_view_unstaged">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">True</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
+  </template>
+</interface>
\ No newline at end of file
diff --git a/gitg/resources/ui/gitg-commit-submodule-info.ui b/gitg/resources/ui/gitg-commit-submodule-info.ui
new file mode 100644
index 0000000..395f428
--- /dev/null
+++ b/gitg/resources/ui/gitg-commit-submodule-info.ui
@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.3"/>
+  <requires lib="gitg" version="3.0"/>
+  <template class="GitgCommitSubmoduleInfo" parent="GtkGrid">
+    <property name="row_spacing">6</property>
+    <property name="column_spacing">6</property>
+    <child>
+      <object class="GtkImage" id="image_folder">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="valign">start</property>
+        <property name="icon_name">folder-remote</property>
+        <property name="icon_size">6</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_path">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="selectable">True</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+        <style>
+          <class name="title-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_url">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="selectable">True</property>
+        <property name="halign">start</property>
+        <property name="hexpand">True</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_sha1">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="selectable">True</property>
+        <property name="halign">end</property>
+        <property name="hexpand">True</property>
+        <style>
+          <class name="sha1-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">3</property>
+        <property name="width">3</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="button_open">
+        <property name="label" translatable="yes">Open</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="valign">start</property>
+        <signal name="clicked" handler="on_open_button_clicked"/>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">0</property>
+        <property name="height">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label_subject">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="selectable">True</property>
+        <property name="halign">start</property>
+        <property name="margin_left">12</property>
+        <property name="margin_top">12</property>
+        <property name="hexpand">True</property>
+        <style>
+          <class name="subject-label"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">3</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/gitg/resources/ui/style.css b/gitg/resources/ui/style.css
index b0d8e1b..101caac 100644
--- a/gitg/resources/ui/style.css
+++ b/gitg/resources/ui/style.css
@@ -120,3 +120,17 @@ GitgHistoryRefRow {
        border: 0;
        -GtkWidget-focus-padding: 0;
 }
+
+.title-label {
+       font-weight: bold;
+}
+
+.subject-label {
+       font-weight: bold;
+}
+
+.sha1-label {
+       font-size: 0.8em;
+       color: @insensitive_fg_color;
+       font-family: monospace;
+}
\ No newline at end of file
diff --git a/libgitg/gitg-stage-status-enumerator.vala b/libgitg/gitg-stage-status-enumerator.vala
index b767330..5b86f17 100644
--- a/libgitg/gitg-stage-status-enumerator.vala
+++ b/libgitg/gitg-stage-status-enumerator.vala
@@ -20,11 +20,37 @@
 namespace Gitg
 {
 
-public class StageStatusFile : Object
+public interface StageStatusItem : Object
+{
+       public abstract string path { owned get; }
+
+       public abstract bool is_staged { get; }
+       public abstract bool is_unstaged { get; }
+       public abstract bool is_untracked { get; }
+
+       public abstract string? icon_name { owned get; }
+}
+
+public class StageStatusFile : Object, StageStatusItem
 {
        private string d_path;
        private Ggit.StatusFlags d_flags;
 
+       private static Ggit.StatusFlags s_index_flags =
+                 Ggit.StatusFlags.INDEX_NEW
+               | Ggit.StatusFlags.INDEX_MODIFIED
+               | Ggit.StatusFlags.INDEX_DELETED
+               | Ggit.StatusFlags.INDEX_RENAMED
+               | Ggit.StatusFlags.INDEX_TYPECHANGE;
+
+       private static Ggit.StatusFlags s_work_flags =
+                 Ggit.StatusFlags.WORKING_TREE_MODIFIED
+               | Ggit.StatusFlags.WORKING_TREE_DELETED
+               | Ggit.StatusFlags.WORKING_TREE_TYPECHANGE;
+
+       private static Ggit.StatusFlags s_untracked_flags =
+                 Ggit.StatusFlags.WORKING_TREE_NEW;
+
        public StageStatusFile(string path, Ggit.StatusFlags flags)
        {
                d_path = path;
@@ -36,17 +62,144 @@ public class StageStatusFile : Object
                owned get { return d_path; }
        }
 
+       public bool is_staged
+       {
+               get { return (d_flags & s_index_flags) != 0; }
+       }
+
+       public bool is_unstaged
+       {
+               get { return (d_flags & s_work_flags) != 0; }
+       }
+
+       public bool is_untracked
+       {
+               get { return (d_flags & s_untracked_flags) != 0; }
+       }
+
        public Ggit.StatusFlags flags
        {
                get { return d_flags; }
        }
+
+       private string? icon_for_status(Ggit.StatusFlags status)
+       {
+               if ((status & (Ggit.StatusFlags.INDEX_NEW |
+                                  Ggit.StatusFlags.WORKING_TREE_NEW)) != 0)
+               {
+                       return "list-add-symbolic";
+               }
+               else if ((status & (Ggit.StatusFlags.INDEX_MODIFIED |
+                                       Ggit.StatusFlags.INDEX_RENAMED |
+                                       Ggit.StatusFlags.INDEX_TYPECHANGE |
+                                       Ggit.StatusFlags.WORKING_TREE_MODIFIED |
+                                       Ggit.StatusFlags.WORKING_TREE_TYPECHANGE)) != 0)
+               {
+                       return "text-editor-symbolic";
+               }
+               else if ((status & (Ggit.StatusFlags.INDEX_DELETED |
+                                       Ggit.StatusFlags.WORKING_TREE_DELETED)) != 0)
+               {
+                       return "edit-delete-symbolic";
+               }
+
+               return null;
+       }
+
+       public string? icon_name
+       {
+               owned get { return icon_for_status(d_flags); }
+       }
+}
+
+public class StageStatusSubmodule : Object, StageStatusItem
+{
+       private Ggit.Submodule d_submodule;
+       private string d_path;
+       private Ggit.SubmoduleStatus d_flags;
+
+       private static Ggit.SubmoduleStatus s_index_flags =
+                 Ggit.SubmoduleStatus.INDEX_ADDED
+               | Ggit.SubmoduleStatus.INDEX_DELETED
+               | Ggit.SubmoduleStatus.INDEX_MODIFIED;
+
+       private static Ggit.SubmoduleStatus s_work_flags =
+                 Ggit.SubmoduleStatus.WD_ADDED
+               | Ggit.SubmoduleStatus.WD_DELETED
+               | Ggit.SubmoduleStatus.WD_MODIFIED;
+
+       private static Ggit.SubmoduleStatus s_untracked_flags =
+                 Ggit.SubmoduleStatus.IN_WD;
+
+       private static Ggit.SubmoduleStatus s_tracked_flags =
+                 Ggit.SubmoduleStatus.IN_HEAD
+               | Ggit.SubmoduleStatus.IN_INDEX;
+
+       private static Ggit.SubmoduleStatus s_dirty_flags =
+                 Ggit.SubmoduleStatus.WD_INDEX_MODIFIED
+               | Ggit.SubmoduleStatus.WD_WD_MODIFIED;
+
+       public StageStatusSubmodule(Ggit.Submodule submodule)
+       {
+               d_submodule = submodule;
+
+               d_path = submodule.get_path();
+
+               try
+               {
+                       d_flags = submodule.get_status();
+               } catch {}
+       }
+
+       public Ggit.Submodule submodule
+       {
+               get { return d_submodule; }
+       }
+
+       public string path
+       {
+               owned get { return d_path; }
+       }
+
+       public bool is_staged
+       {
+               get { return (d_flags & s_index_flags) != 0; }
+       }
+
+       public bool is_unstaged
+       {
+               get { return (d_flags & s_work_flags) != 0; }
+       }
+
+       public bool is_untracked
+       {
+               get
+               {
+                       return    (d_flags & s_untracked_flags) != 0
+                              && (d_flags & s_tracked_flags) == 0;
+               }
+       }
+
+       public bool is_dirty
+       {
+               get { return (d_flags & s_dirty_flags) != 0; }
+       }
+
+       public Ggit.SubmoduleStatus flags
+       {
+               get { return d_flags; }
+       }
+
+       public string? icon_name {
+               owned get { return "folder-remote-symbolic"; }
+       }
 }
 
 public class StageStatusEnumerator : Object
 {
        private Repository d_repository;
        private Thread<void *> d_thread;
-       private StageStatusFile[] d_files;
+       private StageStatusItem[] d_items;
        private int d_offset;
        private int d_callback_num;
        private Cancellable d_cancellable;
@@ -59,8 +212,8 @@ public class StageStatusEnumerator : Object
                d_repository = repository;
                d_options = options;
 
-               d_files = new StageStatusFile[100];
-               d_files.length = 0;
+               d_items = new StageStatusItem[100];
+               d_items.length = 0;
                d_cancellable = new Cancellable();
 
                try
@@ -71,7 +224,7 @@ public class StageStatusEnumerator : Object
 
        public void cancel()
        {
-               lock (d_files)
+               lock (d_items)
                {
                        if (d_cancellable != null)
                        {
@@ -86,34 +239,45 @@ public class StageStatusEnumerator : Object
                }
        }
 
+       private delegate void AddItem(StageStatusItem item);
+
        private void *run_status()
        {
+               AddItem add = (item) => {
+                       lock (d_items)
+                       {
+                               d_items += item;
+
+                               if (d_callback != null && d_callback_num != -1 && d_items.length >= 
d_callback_num)
+                               {
+                                       var cb = (owned)d_callback;
+                                       d_callback = null;
+
+                                       Idle.add((owned)cb);
+                               }
+                       }
+               };
+
                try
                {
                        d_repository.file_status_foreach(d_options, (path, flags) => {
-                               lock (d_files)
-                               {
-                                       d_files += new StageStatusFile(path, flags);
+                               add(new StageStatusFile(path, flags));
 
-                                       if (d_callback != null && d_callback_num != -1 && d_files.length >= 
d_callback_num)
-                                       {
-                                               var cb = (owned)d_callback;
-                                               d_callback = null;
-
-                                               Idle.add((owned)cb);
-                                       }
-                               }
+                               return d_cancellable.is_cancelled() ? 1 : 0;
+                       });
+               } catch {}
 
-                               if (d_cancellable.is_cancelled())
-                               {
-                                       return 1;
-                               }
+               try
+               {
+                       d_repository.submodule_foreach((submodule) => {
+                               submodule.set_ignore(Ggit.SubmoduleIgnore.UNTRACKED);
+                               add(new StageStatusSubmodule(submodule));
 
-                               return 0;
+                               return d_cancellable.is_cancelled() ? 1 : 0;
                        });
                } catch {}
 
-               lock (d_files)
+               lock (d_items)
                {
                        d_cancellable = null;
 
@@ -129,27 +293,27 @@ public class StageStatusEnumerator : Object
                return null;
        }
 
-       private StageStatusFile[] fill_files(int num)
+       private StageStatusItem[] fill_items(int num)
        {
                int n = 0;
 
                if (num == -1)
                {
-                       num = d_files.length - d_offset;
+                       num = d_items.length - d_offset;
                }
 
-               StageStatusFile[] ret = new StageStatusFile[int.min(num, d_files.length - d_offset)];
+               StageStatusItem[] ret = new StageStatusItem[int.min(num, d_items.length - d_offset)];
                ret.length = 0;
 
-               // d_files is already locked here, so it's safe to access
-               while (d_offset < d_files.length)
+               // d_items is already locked here, so it's safe to access
+               while (d_offset < d_items.length)
                {
                        if (n == num)
                        {
                                break;
                        }
 
-                       ret += d_files[d_offset];
+                       ret += d_items[d_offset];
                        d_offset++;
 
                        ++n;
@@ -158,17 +322,17 @@ public class StageStatusEnumerator : Object
                return ret;
        }
 
-       public async StageStatusFile[] next_files(int num)
+       public async StageStatusItem[] next_items(int num)
        {
-               SourceFunc callback = next_files.callback;
-               StageStatusFile[] ret;
+               SourceFunc callback = next_items.callback;
+               StageStatusItem[] ret;
 
-               lock (d_files)
+               lock (d_items)
                {
                        if (d_cancellable == null)
                        {
                                // Already finished
-                               return fill_files(num);
+                               return fill_items(num);
                        }
                        else
                        {
@@ -179,9 +343,9 @@ public class StageStatusEnumerator : Object
 
                yield;
 
-               lock (d_files)
+               lock (d_items)
                {
-                       ret = fill_files(num);
+                       ret = fill_items(num);
                }
 
                if (ret.length != num)
diff --git a/libgitg/gitg-stage.vala b/libgitg/gitg-stage.vala
index fa4128d..687d879 100644
--- a/libgitg/gitg-stage.vala
+++ b/libgitg/gitg-stage.vala
@@ -904,8 +904,8 @@ public class Stage : Object
                });
        }
 
-       public async Ggit.Diff? diff_index_all(StageStatusFile[] files,
-                                              Ggit.DiffOptions? defopts = null) throws Error
+       public async Ggit.Diff? diff_index_all(StageStatusItem[]? files,
+                                              Ggit.DiffOptions?  defopts = null) throws Error
        {
                var opts = new Ggit.DiffOptions();
 
@@ -913,14 +913,18 @@ public class Stage : Object
                             Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
                             Ggit.DiffOption.RECURSE_UNTRACKED_DIRS;
 
-               var pspec = new string[files.length];
 
-               for (var i = 0; i < files.length; i++)
+               if (files != null)
                {
-                       pspec[i] = files[i].path;
-               }
+                       var pspec = new string[files.length];
+
+                       for (var i = 0; i < files.length; i++)
+                       {
+                               pspec[i] = files[i].path;
+                       }
 
-               opts.pathspec = pspec;
+                       opts.pathspec = pspec;
+               }
 
                if (defopts != null)
                {
@@ -941,13 +945,13 @@ public class Stage : Object
                                                   opts);
        }
 
-       public async Ggit.Diff? diff_index(StageStatusFile   f,
+       public async Ggit.Diff? diff_index(StageStatusItem   f,
                                           Ggit.DiffOptions? defopts = null) throws Error
        {
-               return yield diff_index_all(new StageStatusFile[] {f}, defopts);
+               return yield diff_index_all(new StageStatusItem[] {f}, defopts);
        }
 
-       public async Ggit.Diff? diff_workdir_all(StageStatusFile[] files,
+       public async Ggit.Diff? diff_workdir_all(StageStatusItem[] files,
                                                 Ggit.DiffOptions? defopts = null) throws Error
        {
                var opts = new Ggit.DiffOptions();
@@ -956,14 +960,17 @@ public class Stage : Object
                             Ggit.DiffOption.DISABLE_PATHSPEC_MATCH |
                             Ggit.DiffOption.RECURSE_UNTRACKED_DIRS;
 
-               var pspec = new string[files.length];
-
-               for (var i = 0; i < files.length; i++)
+               if (files != null)
                {
-                       pspec[i] = files[i].path;
-               }
+                       var pspec = new string[files.length];
 
-               opts.pathspec = pspec;
+                       for (var i = 0; i < files.length; i++)
+                       {
+                               pspec[i] = files[i].path;
+                       }
+
+                       opts.pathspec = pspec;
+               }
 
                if (defopts != null)
                {
@@ -981,10 +988,10 @@ public class Stage : Object
                                                      opts);
        }
 
-       public async Ggit.Diff? diff_workdir(StageStatusFile   f,
+       public async Ggit.Diff? diff_workdir(StageStatusItem   f,
                                             Ggit.DiffOptions? defopts = null) throws Error
        {
-               return yield diff_workdir_all(new StageStatusFile[] {f}, defopts);
+               return yield diff_workdir_all(new StageStatusItem[] {f}, defopts);
        }
 }
 
diff --git a/tests/libgitg/test-stage.vala b/tests/libgitg/test-stage.vala
index 0e83cb8..9bd1175 100644
--- a/tests/libgitg/test-stage.vala
+++ b/tests/libgitg/test-stage.vala
@@ -52,13 +52,15 @@ class Gitg.Test.Stage : Gitg.Test.Repository
                var stage = d_repository.stage;
                var e = stage.file_status(null);
 
-               e.next_files.begin(-1, (obj, res) => {
-                       var files = e.next_files.end(res);
+               e.next_items.begin(-1, (obj, res) => {
+                       var items = e.next_items.end(res);
 
-                       assert(files.length == cfiles.size);
+                       assert(items.length == cfiles.size);
 
-                       foreach (var f in files)
+                       foreach (var item in items)
                        {
+                               var f = item as Gitg.StageStatusFile;
+
                                assert(cfiles.has_key(f.path));
                                assert_inteq(cfiles[f.path], f.flags);
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]