Detect the git command in configure-time
[giggle.git] / src / giggle-git.c
blob73320eeb6776a63a1fd6dec1e19bba82147dd14c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2007 Imendio AB
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (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 GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public
16 * License along with this program; if not, write to the
17 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
21 #include <config.h>
23 #include "giggle-dispatcher.h"
24 #include "giggle-git.h"
25 #include "giggle-remote.h"
27 #define d(x) x
29 typedef struct GiggleGitPriv GiggleGitPriv;
31 struct GiggleGitPriv {
32 GiggleDispatcher *dispatcher;
33 gchar *directory;
34 gchar *git_dir;
35 gchar *project_dir;
36 gchar *project_name;
37 gchar *description;
39 GList *remotes;
41 GHashTable *jobs;
44 typedef struct {
45 guint id;
46 GiggleJob *job;
47 GiggleJobDoneCallback callback;
48 gpointer user_data;
49 GDestroyNotify destroy_notify;
50 } GitJobData;
52 static void git_finalize (GObject *object);
53 static void git_get_property (GObject *object,
54 guint param_id,
55 GValue *value,
56 GParamSpec *pspec);
57 static void git_set_property (GObject *object,
58 guint param_id,
59 const GValue *value,
60 GParamSpec *pspec);
61 static gboolean git_verify_directory (GiggleGit *git,
62 const gchar *directory,
63 gchar **git_dir,
64 GError **error);
65 static void git_job_data_free (GitJobData *data);
66 static void git_execute_callback (GiggleDispatcher *dispatcher,
67 guint id,
68 GError *error,
69 const gchar *output_str,
70 gsize output_len,
71 GiggleGit *git);
72 static GQuark giggle_git_error_quark (void);
74 G_DEFINE_TYPE (GiggleGit, giggle_git, G_TYPE_OBJECT)
76 enum {
77 PROP_0,
78 PROP_DESCRIPTION,
79 PROP_DIRECTORY,
80 PROP_GIT_DIR,
81 PROP_PROJECT_DIR,
82 PROP_PROJECT_NAME,
83 PROP_REMOTES
86 #define GET_PRIV(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GIGGLE_TYPE_GIT, GiggleGitPriv))
88 static void
89 giggle_git_class_init (GiggleGitClass *class)
91 GObjectClass *object_class = G_OBJECT_CLASS (class);
93 object_class->finalize = git_finalize;
94 object_class->get_property = git_get_property;
95 object_class->set_property = git_set_property;
97 g_object_class_install_property (object_class,
98 PROP_DESCRIPTION,
99 g_param_spec_string ("description",
100 "Description",
101 "The project's description",
102 NULL,
103 G_PARAM_READABLE));
104 g_object_class_install_property (object_class,
105 PROP_DIRECTORY,
106 g_param_spec_string ("directory",
107 "Directory",
108 "the working directory",
109 NULL,
110 G_PARAM_READABLE));
111 g_object_class_install_property (object_class,
112 PROP_GIT_DIR,
113 g_param_spec_string ("git-dir",
114 "Git-Directory",
115 "The equivalent of $GIT_DIR",
116 NULL,
117 G_PARAM_READABLE));
118 g_object_class_install_property (object_class,
119 PROP_PROJECT_DIR,
120 g_param_spec_string ("project-dir",
121 "Project Directory",
122 "The location of the checkout currently being worked on",
123 NULL,
124 G_PARAM_READABLE));
125 g_object_class_install_property (object_class,
126 PROP_PROJECT_NAME,
127 g_param_spec_string ("project-name",
128 "Project Name",
129 "The name of the project (guessed)",
130 NULL,
131 G_PARAM_READABLE));
132 g_object_class_install_property (object_class,
133 PROP_REMOTES,
134 g_param_spec_string ("remotes",
135 "Remotes",
136 "The remote sources",
137 NULL, G_PARAM_READABLE));
139 g_type_class_add_private (object_class, sizeof (GiggleGitPriv));
142 static void
143 giggle_git_init (GiggleGit *git)
145 GiggleGitPriv *priv;
147 priv = GET_PRIV (git);
149 priv->directory = NULL;
150 priv->dispatcher = giggle_dispatcher_new ();
152 priv->jobs = g_hash_table_new_full (g_direct_hash, g_direct_equal,
153 NULL,
154 (GDestroyNotify) git_job_data_free);
157 static void
158 foreach_job_cancel (gpointer key, GitJobData *data, GiggleGit *git)
160 GiggleGitPriv *priv;
162 priv = GET_PRIV (git);
164 giggle_dispatcher_cancel (priv->dispatcher, data->id);
167 static void
168 git_finalize (GObject *object)
170 GiggleGitPriv *priv;
172 priv = GET_PRIV (object);
174 g_hash_table_foreach (priv->jobs,
175 (GHFunc) foreach_job_cancel,
176 object);
178 g_hash_table_destroy (priv->jobs);
179 g_free (priv->directory);
180 g_free (priv->git_dir);
181 g_free (priv->project_dir);
182 g_free (priv->project_name);
184 g_object_unref (priv->dispatcher);
186 G_OBJECT_CLASS (giggle_git_parent_class)->finalize (object);
189 static void
190 git_get_property (GObject *object,
191 guint param_id,
192 GValue *value,
193 GParamSpec *pspec)
195 GiggleGitPriv *priv;
197 priv = GET_PRIV (object);
199 switch (param_id) {
200 case PROP_DESCRIPTION:
201 g_value_set_string (value, priv->description);
202 break;
203 case PROP_DIRECTORY:
204 g_value_set_string (value, priv->directory);
205 break;
206 case PROP_GIT_DIR:
207 g_value_set_string (value, priv->git_dir);
208 break;
209 case PROP_PROJECT_DIR:
210 g_value_set_string (value, priv->project_dir);
211 break;
212 case PROP_PROJECT_NAME:
213 g_value_set_string (value, priv->project_name);
214 break;
215 case PROP_REMOTES:
216 g_value_set_pointer (value, priv->remotes);
217 break;
218 default:
219 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
220 break;
224 static void
225 git_set_property (GObject *object,
226 guint param_id,
227 const GValue *value,
228 GParamSpec *pspec)
230 GiggleGitPriv *priv;
232 priv = GET_PRIV (object);
234 switch (param_id) {
235 default:
236 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
237 break;
241 static GQuark
242 giggle_git_error_quark (void)
244 return g_quark_from_string("GiggleGitError");
247 static gboolean
248 git_verify_directory (GiggleGit *git,
249 const gchar *directory,
250 gchar **git_dir,
251 GError **error)
253 /* Do some funky stuff to verify that it's a valid GIT repo */
254 gchar *argv[] = { GIT_COMMAND, "rev-parse", "--git-dir", NULL };
255 gchar *std_out = NULL;
256 gchar *std_err = NULL;
257 gint exit_code = 0;
258 gboolean verified = FALSE;
259 GError *local_error = NULL;
261 if(git_dir) {
262 *git_dir = NULL;
265 g_spawn_sync (directory, argv, NULL,
266 0, NULL, NULL,
267 &std_out, &std_err,
268 &exit_code, &local_error);
270 if (local_error) {
271 if (error) {
272 *error = local_error;
273 } else {
274 g_warning ("Problem while checking folder \"%s\" for being related to git: %s",
275 directory,
276 local_error->message);
277 g_error_free (local_error);
279 } else if (exit_code != 0) {
280 if (error) {
281 g_set_error (error, giggle_git_error_quark(), 0 /* error code */, "%s", std_err);
282 } else {
283 g_warning ("Problem while checking folder \"%s\": Unexpected exit code %d: %s",
284 directory, exit_code, std_err);
286 } else {
287 verified = TRUE;
289 if (git_dir) {
290 /* split into {dir, NULL} */
291 gchar** split = g_strsplit (std_out, "\n", 2);
292 if(!split || !*split) {
293 g_warning("Didn't get a good git directory for %s: %s", directory, std_out);
295 *git_dir = g_strdup(split ? *split : "");
296 g_strfreev (split);
298 if(!g_path_is_absolute(*git_dir)) {
299 gchar* full_path = g_build_path(G_DIR_SEPARATOR_S, directory, *git_dir, NULL);
300 g_free(*git_dir);
301 *git_dir = full_path;
306 g_free(std_out);
307 g_free(std_err);
309 return verified;
312 static void
313 git_job_data_free (GitJobData *data)
315 g_object_unref (data->job);
317 g_slice_free (GitJobData, data);
320 static void
321 git_execute_callback (GiggleDispatcher *dispatcher,
322 guint id,
323 GError *error,
324 const gchar *output_str,
325 gsize output_len,
326 GiggleGit *git)
328 GiggleGitPriv *priv;
329 GitJobData *data;
331 priv = GET_PRIV (git);
333 data = g_hash_table_lookup (priv->jobs, GINT_TO_POINTER (id));
334 g_assert (data != NULL);
336 if (!error) {
337 giggle_job_handle_output (data->job, output_str, output_len);
340 if (data->callback) {
341 data->callback (git, data->job, error, data->user_data);
344 if (data->destroy_notify && data->user_data) {
345 (data->destroy_notify) (data->user_data);
348 g_hash_table_remove (priv->jobs, GINT_TO_POINTER (id));
351 GiggleGit *
352 giggle_git_get (void)
354 static GiggleGit *git = NULL;
356 if (!git) {
357 git = g_object_new (GIGGLE_TYPE_GIT, NULL);
358 } else {
359 g_object_ref (git);
362 return git;
365 static gchar *
366 giggle_git_get_description_file (const GiggleGit *git)
368 GiggleGitPriv *priv = GET_PRIV (git);
370 return g_build_filename (priv->git_dir, "description", NULL);
373 static void
374 giggle_git_update_description (GiggleGit *git)
376 // FIXME: read .git/description into description; install a file watch
377 GiggleGitPriv *priv;
378 GError *error;
379 gchar* description;
381 priv = GET_PRIV (git);
382 g_free (priv->description);
383 priv->description = NULL;
385 description = giggle_git_get_description_file (git);
386 error = NULL;
387 if (!g_file_get_contents (description, &(priv->description), NULL, &error)) {
388 if (error) {
389 g_warning ("Couldn't read description file %s: %s", description, error->message);
390 g_error_free (error);
391 } else {
392 g_warning ("Couldn't read description file %s", description);
394 if(!priv->description) {
395 priv->description = g_strdup ("");
399 g_free (description);
401 g_object_notify (G_OBJECT (git), "description");
404 static void
405 giggle_git_update_remotes (GiggleGit* git)
407 GiggleGitPriv *priv;
408 GError *error;
409 gchar *path;
410 GDir *dir;
412 priv = GET_PRIV (git);
414 /* cleanup */
415 g_list_foreach (priv->remotes, (GFunc) g_object_unref, NULL);
416 g_list_free (priv->remotes);
417 priv->remotes = NULL;
419 /* list files and add them */
420 error = NULL;
421 path = g_build_filename (priv->git_dir, "remotes", NULL);
422 dir = g_dir_open (path, 0, &error);
424 if(error) {
425 g_warning ("Error loading remotes: %s", error->message);
426 } else {
427 const gchar *file;
429 for(file = g_dir_read_name(dir); file; file = g_dir_read_name(dir)) {
430 gchar *filename = g_build_filename (path, file, NULL);
431 priv->remotes = g_list_prepend (priv->remotes, giggle_remote_new_from_file (filename));
432 g_free (filename);
435 g_dir_close (dir);
437 priv->remotes = g_list_reverse (priv->remotes);
438 g_free (path);
440 /* update */
441 g_object_notify (G_OBJECT (git), "remotes");
444 const gchar *
445 giggle_git_get_description (GiggleGit *git)
447 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
449 return GET_PRIV(git)->description;
452 void
453 giggle_git_write_description (GiggleGit *git,
454 const gchar *description) // FIXME: add GError?
456 GiggleGitPriv *priv;
457 GError *error;
458 gchar *filename;
460 g_return_if_fail (GIGGLE_IS_GIT (git));
462 priv = GET_PRIV (git);
463 if(description == priv->description) {
464 return;
467 g_free (priv->description);
468 priv->description = g_strdup (description);
470 error = NULL;
471 filename = giggle_git_get_description_file (git);
472 if (!g_file_set_contents (filename, priv->description, -1, &error)) {
473 if (error) {
474 g_warning ("Couldn't write description: %s", error->message);
475 g_error_free(error);
476 } else {
477 g_warning ("Couldn't write description");
480 g_free (filename);
482 /* notify will become unnecessary once we have file monitoring */
483 g_object_notify (G_OBJECT (git), "description");
486 const gchar *
487 giggle_git_get_directory (GiggleGit *git)
489 GiggleGitPriv *priv;
491 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
493 priv = GET_PRIV (git);
495 return priv->directory;
498 gboolean
499 giggle_git_set_directory (GiggleGit *git,
500 const gchar *directory,
501 GError **error)
503 GiggleGitPriv *priv;
504 gchar *tmp_dir;
505 gchar *suffix;
507 g_return_val_if_fail (GIGGLE_IS_GIT (git), FALSE);
508 g_return_val_if_fail (directory != NULL, FALSE);
510 priv = GET_PRIV (git);
512 if (!git_verify_directory (git, directory, &tmp_dir, error)) {
513 return FALSE;
516 /* update working directory */
517 g_free (priv->directory);
518 priv->directory = g_strdup (directory);
520 /* update git dir */
521 g_free (priv->git_dir);
522 priv->git_dir = tmp_dir;
524 /* update project dir */
525 g_free (priv->project_dir);
527 tmp_dir = g_strdup (priv->git_dir);
528 suffix = g_strrstr (tmp_dir, ".git");
529 if(G_UNLIKELY(!suffix)) {
530 /* Tele-Tubby: Uuh Ooooh */
531 priv->project_dir = NULL;
532 } else {
533 /* .../giggle/.git
534 * ^ */
535 if(*(--suffix) == G_DIR_SEPARATOR) {
536 *suffix = '\0';
537 priv->project_dir = g_strdup (tmp_dir);
538 /* strdup again to skip truncated chars */
539 } else {
540 /* /home/herzi/Hacking/giggle.git - no project folder */
541 priv->project_dir = NULL;
544 g_free (tmp_dir);
546 /* update project name */
547 if (priv->project_dir) {
548 tmp_dir = g_path_get_basename (priv->project_dir);
549 } else {
550 suffix = g_strrstr (priv->git_dir, ".git");
551 if (suffix) {
552 *suffix = '\0';
553 tmp_dir = g_path_get_basename (priv->git_dir);
554 *suffix = '.'; // restore
555 } else {
556 tmp_dir = NULL;
559 g_free (priv->project_name);
560 priv->project_name = tmp_dir;
562 /* notify */
563 g_object_notify (G_OBJECT (git), "directory");
564 g_object_notify (G_OBJECT (git), "git-dir");
565 g_object_notify (G_OBJECT (git), "project-dir");
566 g_object_notify (G_OBJECT (git), "project-name");
568 giggle_git_update_description (git);
569 giggle_git_update_remotes (git);
571 return TRUE;
574 const gchar *
575 giggle_git_get_git_dir (GiggleGit *git)
577 GiggleGitPriv *priv;
579 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
581 priv = GET_PRIV (git);
583 return priv->git_dir;
586 const gchar *
587 giggle_git_get_project_dir (GiggleGit *git)
589 GiggleGitPriv *priv;
591 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
593 priv = GET_PRIV (git);
595 return priv->project_dir;
598 const gchar *
599 giggle_git_get_project_name (GiggleGit* git)
601 GiggleGitPriv *priv;
603 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
605 priv = GET_PRIV (git);
607 return priv->project_name;
610 GList *
611 giggle_git_get_remotes (GiggleGit *git)
613 g_return_val_if_fail (GIGGLE_IS_GIT (git), NULL);
615 return GET_PRIV (git)->remotes;
618 void
619 giggle_git_save_remote (GiggleGit *git,
620 GiggleRemote*remote)
622 GiggleGitPriv *priv;
623 gchar *path;
625 g_return_if_fail (GIGGLE_IS_GIT (git));
626 g_return_if_fail (GIGGLE_IS_REMOTE (remote));
628 priv = GET_PRIV (git);
629 path = g_build_filename (priv->git_dir, "remotes", giggle_remote_get_name (remote), NULL);
630 giggle_remote_save_to_file (remote, path);
631 g_free (path);
634 void
635 giggle_git_run_job_full (GiggleGit *git,
636 GiggleJob *job,
637 GiggleJobDoneCallback callback,
638 gpointer user_data,
639 GDestroyNotify destroy_notify)
641 GiggleGitPriv *priv;
642 gchar *command;
644 g_return_if_fail (GIGGLE_IS_GIT (git));
645 g_return_if_fail (GIGGLE_IS_JOB (job));
647 priv = GET_PRIV (git);
649 if (giggle_job_get_command_line (job, &command)) {
650 GitJobData *data;
652 data = g_slice_new0 (GitJobData);
653 data->id = giggle_dispatcher_execute (priv->dispatcher,
654 priv->directory,
655 command,
656 (GiggleExecuteCallback) git_execute_callback,
657 git);
659 data->job = g_object_ref (job);
660 data->callback = callback;
661 data->user_data = user_data;
662 data->destroy_notify = destroy_notify;
664 g_object_set (job, "id", data->id, NULL);
666 g_hash_table_insert (priv->jobs,
667 GINT_TO_POINTER (data->id), data);
668 } else {
669 g_warning ("Couldn't get command line for job");
672 g_free (command);
675 void
676 giggle_git_run_job (GiggleGit *git,
677 GiggleJob *job,
678 GiggleJobDoneCallback callback,
679 gpointer user_data)
681 giggle_git_run_job_full (git, job, callback, user_data, NULL);
684 void
685 giggle_git_cancel_job (GiggleGit *git, GiggleJob *job)
687 GiggleGitPriv *priv;
688 guint id;
690 g_return_if_fail (GIGGLE_IS_GIT (git));
691 g_return_if_fail (GIGGLE_IS_JOB (job));
693 priv = GET_PRIV (git);
695 g_object_get (job, "id", &id, NULL);
697 giggle_dispatcher_cancel (priv->dispatcher, id);
699 g_hash_table_remove (priv->jobs, GINT_TO_POINTER (id));