3
* Copyright (C) 2015 Christian Hergert <christian@hergert.me>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 3 of the License, or
8
* (at your option) any later version.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#define G_LOG_DOMAIN "ide-git-vcs"
22
#include <glib/gi18n.h>
23
#include <libgit2-glib/ggit.h>
25
#include "ide-async-helper.h"
26
#include "ide-context.h"
27
#include "ide-debug.h"
28
#include "ide-git-buffer-change-monitor.h"
29
#include "ide-git-vcs.h"
30
#include "ide-project.h"
31
#include "ide-project-file.h"
32
#include "ide-project-files.h"
34
#define DEFAULT_CHANGED_TIMEOUT_SECS 1
38
IdeVcs parent_instance;
40
GgitRepository *repository;
41
GgitRepository *change_monitor_repository;
42
GFile *working_directory;
43
GFileMonitor *monitor;
45
guint changed_timeout;
48
guint loaded_files : 1;
51
static void g_async_initable_init_interface (GAsyncInitableIface *iface);
52
static void ide_git_vcs_reload_async (IdeGitVcs *self,
53
GCancellable *cancellable,
54
GAsyncReadyCallback callback,
56
static gboolean ide_git_vcs_reload_finish (IdeGitVcs *self,
60
G_DEFINE_TYPE_EXTENDED (IdeGitVcs, ide_git_vcs, IDE_TYPE_VCS, 0,
61
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
62
g_async_initable_init_interface))
75
static GParamSpec *gParamSpecs [LAST_PROP];
76
static guint gSignals [LAST_SIGNAL];
79
* ide_git_vcs_get_repository:
81
* Retrieves the underlying #GgitRepository used by @vcs.
83
* Returns: (transfer none): A #GgitRepository.
86
ide_git_vcs_get_repository (IdeGitVcs *self)
88
g_return_val_if_fail (IDE_IS_GIT_VCS (self), NULL);
90
return self->repository;
94
ide_git_vcs_get_working_directory (IdeVcs *vcs)
96
IdeGitVcs *self = (IdeGitVcs *)vcs;
98
g_return_val_if_fail (IDE_IS_GIT_VCS (self), NULL);
100
return self->working_directory;
103
static IdeBufferChangeMonitor *
104
ide_git_vcs_get_buffer_change_monitor (IdeVcs *vcs,
107
IdeGitVcs *self = (IdeGitVcs *)vcs;
110
g_return_val_if_fail (IDE_IS_GIT_VCS (vcs), NULL);
112
context = ide_object_get_context (IDE_OBJECT (vcs));
114
return g_object_new (IDE_TYPE_GIT_BUFFER_CHANGE_MONITOR,
117
"repository", self->change_monitor_repository,
122
ide_git_vcs_load_repository_worker (GTask *task,
123
gpointer source_object,
125
GCancellable *cancellable)
127
GFile *project_file = task_data;
128
g_autoptr(GFile) location = NULL;
129
GgitRepository *repository = NULL;
130
GError *error = NULL;
132
g_assert (G_IS_TASK (task));
133
g_assert (G_IS_FILE (project_file));
135
location = ggit_repository_discover (project_file, &error);
139
g_task_return_error (task, error);
143
repository = ggit_repository_open (location, &error);
147
g_task_return_error (task, error);
151
g_task_return_pointer (task, repository, g_object_unref);
155
ide_git_vcs_load_repository_async (IdeGitVcs *self,
156
GCancellable *cancellable,
157
GAsyncReadyCallback callback,
160
g_autoptr(GTask) task = NULL;
164
g_assert (IDE_IS_GIT_VCS (self));
165
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
167
context = ide_object_get_context (IDE_OBJECT (self));
168
project_file = ide_context_get_project_file (context);
170
task = g_task_new (self, cancellable, callback, user_data);
171
g_task_set_task_data (task, g_object_ref (project_file), g_object_unref);
172
g_task_run_in_thread (task, ide_git_vcs_load_repository_worker);
175
static GgitRepository *
176
ide_git_vcs_load_repository_finish (IdeGitVcs *self,
177
GAsyncResult *result,
180
GTask *task = (GTask *)result;
183
g_assert (IDE_IS_GIT_VCS (self));
185
ret = g_task_propagate_pointer (task, error);
189
GFile *working_directory;
191
working_directory = ggit_repository_get_workdir (ret);
192
g_set_object (&self->working_directory, working_directory);
199
ide_git_vcs_reload_index_add_path (IdeGitVcs *self,
202
const gchar *workdir,
203
gboolean is_directory)
205
IdeProjectItem *parent;
206
IdeProjectItem *item;
208
GFileInfo *file_info = NULL;
210
g_autofree gchar *fullpath = NULL;
214
g_return_if_fail (IDE_IS_GIT_VCS (self));
215
g_return_if_fail (cache);
216
g_return_if_fail (path);
218
context = ide_object_get_context (IDE_OBJECT (self));
220
dir = g_path_get_dirname (path);
221
name = g_path_get_basename (path);
223
parent = g_hash_table_lookup (cache, dir);
227
ide_git_vcs_reload_index_add_path (self, cache, dir, workdir, TRUE);
228
parent = g_hash_table_lookup (cache, dir);
231
g_assert (IDE_IS_PROJECT_ITEM (parent));
233
file_info = g_file_info_new ();
234
g_file_info_set_name (file_info, name);
235
g_file_info_set_display_name (file_info, name);
238
* TODO: We can probably extract some additional information from the
239
* index such as symbolic link, etc.
242
g_file_info_set_file_type (file_info, G_FILE_TYPE_DIRECTORY);
244
fullpath = g_build_filename (workdir, path, NULL);
245
file = g_file_new_for_path (fullpath);
247
item = g_object_new (IDE_TYPE_PROJECT_FILE,
250
"file-info", file_info,
254
ide_project_item_append (parent, item);
256
g_hash_table_insert (cache, g_strdup (path), g_object_ref (item));
258
g_clear_object (&file);
259
g_clear_object (&file_info);
260
g_clear_object (&item);
261
g_clear_pointer (&dir, g_free);
262
g_clear_pointer (&name, g_free);
266
ide_git_vcs_build_tree_worker (GTask *task,
267
gpointer source_object,
269
GCancellable *cancellable)
271
IdeGitVcs *self = source_object;
272
GgitIndexEntries *entries = NULL;
273
GgitRepository *repository = task_data;
274
IdeProjectItem *root;
275
IdeProjectItem *files = NULL;
278
GgitIndex *index = NULL;
279
GHashTable *cache = NULL;
280
g_autofree gchar *workdir = NULL;
281
g_autoptr(GFile) workdir_file = NULL;
282
GError *error = NULL;
286
g_assert (G_IS_TASK (task));
287
g_assert (IDE_IS_GIT_VCS (self));
288
g_assert (GGIT_IS_REPOSITORY (repository));
289
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
291
index = ggit_repository_get_index (repository, &error);
295
entries = ggit_index_get_entries (index);
299
count = ggit_index_entries_size (entries);
300
cache = g_hash_table_new_full (g_str_hash, g_str_equal,
301
g_free, g_object_unref);
303
context = ide_object_get_context (IDE_OBJECT (self));
304
project = ide_context_get_project (context);
306
ide_project_reader_lock (project);
307
root = ide_project_get_root (project);
308
files = g_object_new (IDE_TYPE_PROJECT_FILES,
312
ide_project_reader_unlock (project);
314
g_hash_table_insert (cache, g_strdup ("."), g_object_ref (files));
316
workdir_file = ggit_repository_get_workdir (repository);
317
workdir = g_file_get_path (workdir_file);
319
for (i = 0; i < count; i++)
321
GgitIndexEntry *entry;
324
entry = ggit_index_entries_get_by_index (entries, i);
325
path = ggit_index_entry_get_path (entry);
326
ide_git_vcs_reload_index_add_path (self, cache, path, workdir, FALSE);
327
ggit_index_entry_unref (entry);
332
g_task_return_error (task, error);
334
g_task_return_pointer (task, g_object_ref (files), g_object_unref);
336
g_clear_pointer (&cache, g_hash_table_unref);
337
g_clear_pointer (&entries, ggit_index_entries_unref);
338
g_clear_object (&files);
339
g_clear_object (&index);
343
ide_git_vcs_build_tree_async (IdeGitVcs *self,
344
GgitRepository *repository,
345
GCancellable *cancellable,
346
GAsyncReadyCallback callback,
349
g_autoptr(GTask) task = NULL;
351
g_assert (IDE_IS_GIT_VCS (self));
352
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
354
task = g_task_new (self, cancellable, callback, user_data);
355
g_task_set_task_data (task, g_object_ref (repository), g_object_unref);
356
g_task_run_in_thread (task, ide_git_vcs_build_tree_worker);
359
static IdeProjectFiles *
360
ide_git_vcs_build_tree_finish (IdeGitVcs *self,
361
GAsyncResult *result,
364
GTask *task = (GTask *)result;
366
g_return_val_if_fail (IDE_IS_GIT_VCS (self), NULL);
368
return g_task_propagate_pointer (task, error);
372
ide_git_vcs__reload_cb (GObject *object,
373
GAsyncResult *result,
376
IdeGitVcs *self = (IdeGitVcs *)object;
377
g_autoptr(GError) error = NULL;
379
g_assert (IDE_IS_GIT_VCS (self));
381
if (!ide_git_vcs_reload_finish (self, result, &error))
382
g_message ("%s", error->message);
387
ide_git_vcs__changed_timeout_cb (gpointer user_data)
389
IdeGitVcs *self = user_data;
393
g_assert (IDE_IS_GIT_VCS (self));
395
self->changed_timeout = 0;
396
ide_git_vcs_reload_async (self, NULL, ide_git_vcs__reload_cb, NULL);
398
IDE_RETURN (G_SOURCE_REMOVE);
402
ide_git_vcs__monitor_changed_cb (IdeGitVcs *self,
405
GFileMonitorEvent event_type,
410
g_assert (IDE_IS_GIT_VCS (self));
412
if (self->changed_timeout != 0)
413
g_source_remove (self->changed_timeout);
415
self->changed_timeout = g_timeout_add_seconds (DEFAULT_CHANGED_TIMEOUT_SECS,
416
ide_git_vcs__changed_timeout_cb,
423
ide_git_vcs_load_monitor (IdeGitVcs *self,
428
g_assert (IDE_IS_GIT_VCS (self));
430
if (self->monitor == NULL)
432
g_autoptr(GFile) location = NULL;
433
g_autoptr(GFileMonitor) monitor = NULL;
434
g_autoptr(GFile) heads_dir = NULL;
435
GFileMonitorFlags flags = G_FILE_MONITOR_WATCH_MOUNTS;
437
location = ggit_repository_get_location (self->repository);
438
heads_dir = g_file_get_child (location, "refs/heads");
439
monitor = g_file_monitor (heads_dir, flags, NULL, error);
445
IDE_TRACE_MSG ("Git index monitor registered.");
446
g_signal_connect_object (monitor,
448
G_CALLBACK (ide_git_vcs__monitor_changed_cb),
451
self->monitor = g_object_ref (monitor);
459
ide_git_vcs_reload__load_repository_cb3 (GObject *object,
460
GAsyncResult *result,
463
IdeGitVcs *self = (IdeGitVcs *)object;
464
g_autoptr(GTask) task = user_data;
465
GgitRepository *repository;
466
GError *error = NULL;
468
g_assert (IDE_IS_GIT_VCS (self));
469
g_assert (G_IS_ASYNC_RESULT (result));
471
repository = ide_git_vcs_load_repository_finish (self, result, &error);
475
g_task_return_error (task, error);
479
g_set_object (&self->change_monitor_repository, repository);
482
* Now finally, load the change monitor so that we can detect future changes.
484
if (!ide_git_vcs_load_monitor (self, &error))
486
g_task_return_error (task, error);
490
g_task_return_boolean (task, TRUE);
494
ide_git_vcs_reload__build_tree_cb (GObject *object,
495
GAsyncResult *result,
498
IdeGitVcs *self = (IdeGitVcs *)object;
499
g_autoptr(GTask) task = user_data;
500
g_autoptr(IdeProjectFiles) files = NULL;
501
GError *error = NULL;
503
g_assert (IDE_IS_GIT_VCS (self));
504
g_assert (G_IS_TASK (task));
506
files = ide_git_vcs_build_tree_finish (self, result, &error);
510
g_task_return_error (task, error);
517
* This is a hack to only load the project files the first time. We need to do this for real
518
* in the project tree to make appropriate events for tree changes.
520
if (!self->loaded_files)
525
IdeProjectItem *root;
527
context = ide_object_get_context (IDE_OBJECT (self));
528
project = ide_context_get_project (context);
532
ide_project_writer_lock (project);
533
root = ide_project_get_root (project);
534
/* TODO: Replace existing item!!! */
535
ide_project_item_append (root, IDE_PROJECT_ITEM (files));
536
ide_project_writer_unlock (project);
539
self->loaded_files = TRUE;
543
* Load the repository a third time for use by the threaded change monitors generating diffs.
544
* I know it seems like a lot of loading, but it is a lot better than the N_FILES repositories
545
* we had open previously.
547
ide_git_vcs_load_repository_async (self,
548
g_task_get_cancellable (task),
549
ide_git_vcs_reload__load_repository_cb3,
550
g_object_ref (task));
554
ide_git_vcs_reload__load_repository_cb2 (GObject *object,
555
GAsyncResult *result,
558
IdeGitVcs *self = (IdeGitVcs *)object;
559
g_autoptr(GTask) task = user_data;
560
GgitRepository *repository;
561
GError *error = NULL;
563
g_assert (IDE_IS_GIT_VCS (self));
564
g_assert (G_IS_ASYNC_RESULT (result));
566
repository = ide_git_vcs_load_repository_finish (self, result, &error);
570
g_task_return_error (task, error);
575
* Now go load the files for the project tree.
577
ide_git_vcs_build_tree_async (self,
579
g_task_get_cancellable (task),
580
ide_git_vcs_reload__build_tree_cb,
581
g_object_ref (task));
583
g_clear_object (&repository);
587
ide_git_vcs_reload__load_repository_cb (GObject *object,
588
GAsyncResult *result,
591
IdeGitVcs *self = (IdeGitVcs *)object;
592
g_autoptr(GTask) task = user_data;
593
GgitRepository *repository;
594
GError *error = NULL;
596
g_assert (IDE_IS_GIT_VCS (self));
597
g_assert (G_IS_ASYNC_RESULT (result));
599
repository = ide_git_vcs_load_repository_finish (self, result, &error);
603
g_task_return_error (task, error);
607
g_set_object (&self->repository, repository);
610
* Now load the repository again for use by the threaded index builder.
612
ide_git_vcs_load_repository_async (self,
613
g_task_get_cancellable (task),
614
ide_git_vcs_reload__load_repository_cb2,
615
g_object_ref (task));
619
ide_git_vcs_reload_async (IdeGitVcs *self,
620
GCancellable *cancellable,
621
GAsyncReadyCallback callback,
624
g_autoptr(GTask) task = NULL;
628
g_assert (IDE_IS_GIT_VCS (self));
629
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
631
task = g_task_new (self, cancellable, callback, user_data);
636
* Ignore if we are already reloading. We should probably set a bit here and attept to
637
* reload again after the current process completes.
639
g_task_return_boolean (task, TRUE);
643
self->reloading = TRUE;
645
ide_git_vcs_load_repository_async (self,
647
ide_git_vcs_reload__load_repository_cb,
648
g_object_ref (task));
654
ide_git_vcs_reload_finish (IdeGitVcs *self,
655
GAsyncResult *result,
658
GTask *task = (GTask *)result;
663
g_return_val_if_fail (IDE_IS_GIT_VCS (self), FALSE);
665
self->reloading = FALSE;
666
g_signal_emit (self, gSignals [RELOADED], 0, self->change_monitor_repository);
667
ret = g_task_propagate_boolean (task, error);
673
ide_git_vcs_is_ignored (IdeVcs *vcs,
677
g_autofree gchar *name = NULL;
678
IdeGitVcs *self = (IdeGitVcs *)vcs;
679
gboolean ret = FALSE;
681
g_assert (IDE_IS_GIT_VCS (self));
682
g_assert (G_IS_FILE (file));
684
name = g_file_get_relative_path (self->working_directory, file);
685
if (g_strcmp0 (name, ".git") == 0)
689
return ggit_repository_path_is_ignored (self->repository, name, error);
695
ide_git_vcs_dispose (GObject *object)
697
IdeGitVcs *self = (IdeGitVcs *)object;
701
if (self->changed_timeout)
703
g_source_remove (self->changed_timeout);
704
self->changed_timeout = 0;
709
if (!g_file_monitor_is_cancelled (self->monitor))
710
g_file_monitor_cancel (self->monitor);
711
g_clear_object (&self->monitor);
714
g_clear_object (&self->change_monitor_repository);
715
g_clear_object (&self->repository);
716
g_clear_object (&self->working_directory);
718
G_OBJECT_CLASS (ide_git_vcs_parent_class)->dispose (object);
724
ide_git_vcs_get_property (GObject *object,
729
IdeGitVcs *self = IDE_GIT_VCS (object);
733
case PROP_REPOSITORY:
734
g_value_set_object (value, ide_git_vcs_get_repository (self));
738
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
743
ide_git_vcs_class_init (IdeGitVcsClass *klass)
745
GObjectClass *object_class = G_OBJECT_CLASS (klass);
746
IdeVcsClass *vcs_class = IDE_VCS_CLASS (klass);
748
object_class->dispose = ide_git_vcs_dispose;
749
object_class->get_property = ide_git_vcs_get_property;
751
vcs_class->get_working_directory = ide_git_vcs_get_working_directory;
752
vcs_class->get_buffer_change_monitor = ide_git_vcs_get_buffer_change_monitor;
753
vcs_class->is_ignored = ide_git_vcs_is_ignored;
756
* IdeGitVcs:repository:
758
* This property contains the underlying #GgitRepository that can be used to lookup git
759
* information. Consumers should be careful about using this directly. It is not thread-safe
760
* to use this object, nor is it safe to perform many blocking calls from the main thread.
762
* You might want to get the #GgitRepository:location property and create your own instance
763
* of the repository for threaded operations.
765
gParamSpecs [PROP_REPOSITORY] =
766
g_param_spec_object ("repository",
768
_("The git repository for the project."),
769
GGIT_TYPE_REPOSITORY,
770
(G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
772
g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
775
* IdeGitVcs::reloaded:
776
* @self: An #IdeGitVfs
777
* @repository: A #GgitRepository
779
* This signal is emitted when the git index has been reloaded. Various consumers may want to
780
* reload their git objects upon this notification. Such an example would be the line diffs
781
* that are rendered in the source view gutter.
783
* The @repository instance is to aide consumers in locating the repository and should not
784
* be used directly except in very specific situations. The gutter change renderer uses this
785
* instance in a threaded manner.
787
gSignals [RELOADED] = g_signal_new ("reloaded",
788
G_TYPE_FROM_CLASS (klass),
794
GGIT_TYPE_REPOSITORY);
798
ide_git_vcs_init (IdeGitVcs *self)
803
ide_git_vcs_init_async__reload_cb (GObject *object,
804
GAsyncResult *result,
807
IdeGitVcs *self = (IdeGitVcs *)object;
808
g_autoptr(GTask) task = user_data;
809
GError *error = NULL;
811
g_assert (G_IS_TASK (task));
812
g_assert (IDE_IS_GIT_VCS (self));
814
if (!ide_git_vcs_reload_finish (self, result, &error))
815
g_task_return_error (task, error);
817
g_task_return_boolean (task, TRUE);
821
ide_git_vcs_init_async (GAsyncInitable *initable,
823
GCancellable *cancellable,
824
GAsyncReadyCallback callback,
827
IdeGitVcs *self = (IdeGitVcs *)initable;
828
g_autoptr(GTask) task = NULL;
830
g_return_if_fail (IDE_IS_GIT_VCS (self));
832
task = g_task_new (self, cancellable, callback, user_data);
833
ide_git_vcs_reload_async (self,
835
ide_git_vcs_init_async__reload_cb,
836
g_object_ref (task));
840
ide_git_vcs_init_finish (GAsyncInitable *initable,
841
GAsyncResult *result,
844
GTask *task = (GTask *)result;
846
g_return_val_if_fail (G_IS_TASK (task), FALSE);
848
return g_task_propagate_boolean (task, error);
852
g_async_initable_init_interface (GAsyncInitableIface *iface)
854
iface->init_async = ide_git_vcs_init_async;
855
iface->init_finish = ide_git_vcs_init_finish;