Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_git.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
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.
9 *
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.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
27#ifdef HAVE_LIBGIT2
28
29/* libgit2 *_INIT macros trigger -Wmissing-field-initializers; suppress here */
30#pragma GCC diagnostic push
31#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
32
33#include "nilorea/n_git.h"
34#include "nilorea/n_log.h"
35#include "nilorea/n_common.h"
36
37#include <string.h>
38#include <stdio.h>
39
41static int _n_git_initialized = 0;
42
46static void _n_git_ensure_init(void) {
47 if (!_n_git_initialized) {
48 git_libgit2_init();
50 }
51}
52
57static void _n_git_log_error(const char* func_name) {
58 const git_error* err = git_error_last();
59 if (err) {
60 n_log(LOG_ERR, "%s: %s", func_name, err->message);
61 } else {
62 n_log(LOG_ERR, "%s: unknown libgit2 error", func_name);
63 }
64}
65
70static void _n_git_status_entry_destroy(void* ptr) {
71 // cppcheck-suppress uselessAssignmentPtrArg -- FreeNoLog nulls local copy, intentional for destroy callback API
72 FreeNoLog(ptr);
73}
74
79static void _n_git_commit_info_destroy(void* ptr) {
80 // cppcheck-suppress uselessAssignmentPtrArg -- FreeNoLog nulls local copy, intentional for destroy callback API
81 FreeNoLog(ptr);
82}
83
88static void _n_git_string_destroy(void* ptr) {
89 // cppcheck-suppress uselessAssignmentPtrArg -- FreeNoLog nulls local copy, intentional for destroy callback API
90 FreeNoLog(ptr);
91}
92
101static int _n_git_diff_print_cb(const git_diff_delta* delta, const git_diff_hunk* hunk, const git_diff_line* line, void* payload) {
102 (void)delta;
103 (void)hunk;
104 N_STR** nstr = (N_STR**)payload;
105
106 /* line->content is not null-terminated; copy it */
107 if (line->content && line->content_len > 0) {
108 char* tmp = NULL;
109 Malloc(tmp, char, line->content_len + 2);
110 if (tmp) {
111 memcpy(tmp, line->content, line->content_len);
112 tmp[line->content_len] = '\0';
113 nstrprintf_cat(*nstr, "%c%s", line->origin, tmp);
114 FreeNoLog(tmp);
115 }
116 }
117 return 0;
118}
119
123N_GIT_REPO* n_git_open(const char* path) {
124 __n_assert(path, return NULL);
125
127
128 N_GIT_REPO* ngit = NULL;
129 Malloc(ngit, N_GIT_REPO, 1);
130 __n_assert(ngit, return NULL);
131
132 ngit->path = strdup(path);
133 if (!ngit->path) {
134 n_log(LOG_ERR, "n_git_open: strdup failed");
135 FreeNoLog(ngit);
136 return NULL;
137 }
138
139 int err = git_repository_open(&ngit->repo, path);
140 if (err < 0) {
141 _n_git_log_error("n_git_open");
142 FreeNoLog(ngit->path);
143 FreeNoLog(ngit);
144 return NULL;
145 }
146
147 return ngit;
148} /* n_git_open */
149
155N_GIT_REPO* n_git_init(const char* path) {
156 __n_assert(path, return NULL);
157
159
160 N_GIT_REPO* ngit = NULL;
161 Malloc(ngit, N_GIT_REPO, 1);
162 __n_assert(ngit, return NULL);
163
164 ngit->path = strdup(path);
165 if (!ngit->path) {
166 n_log(LOG_ERR, "n_git_init: strdup failed");
167 FreeNoLog(ngit);
168 return NULL;
169 }
170
171 int err = git_repository_init(&ngit->repo, path, 0);
172 if (err < 0) {
173 _n_git_log_error("n_git_init");
174 FreeNoLog(ngit->path);
175 FreeNoLog(ngit);
176 return NULL;
177 }
178
179 return ngit;
180} /* n_git_init */
181
186 __n_assert(repo, return);
187 __n_assert(*repo, return);
188
189 if ((*repo)->repo) {
190 git_repository_free((*repo)->repo);
191 (*repo)->repo = NULL;
192 }
193 FreeNoLog((*repo)->path);
194 if ((*repo)->remote_auth) {
195 FreeNoLog((*repo)->remote_auth->username);
196 FreeNoLog((*repo)->remote_auth->password);
197 FreeNoLog((*repo)->remote_auth->ssh_pubkey_path);
198 FreeNoLog((*repo)->remote_auth->ssh_privkey_path);
199 FreeNoLog((*repo)->remote_auth->ssh_passphrase);
200 FreeNoLog((*repo)->remote_auth);
201 }
202 FreeNoLog(*repo);
203} /* n_git_close */
204
211 __n_assert(repo, return NULL);
212 __n_assert(repo->repo, return NULL);
213
214 git_status_options opts = GIT_STATUS_OPTIONS_INIT;
215 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
216 opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
217
218 git_status_list* status_list = NULL;
219 int err = git_status_list_new(&status_list, repo->repo, &opts);
220 if (err < 0) {
221 _n_git_log_error("n_git_status");
222 return NULL;
223 }
224
226 if (!list) {
227 git_status_list_free(status_list);
228 return NULL;
229 }
230
231 size_t count = git_status_list_entrycount(status_list);
232 for (size_t i = 0; i < count; i++) {
233 const git_status_entry* entry = git_status_byindex(status_list, i);
234 if (!entry) continue;
235
236 N_GIT_STATUS_ENTRY* se = NULL;
238 if (!se) continue;
239
240 const char* fpath = NULL;
241 if (entry->head_to_index && entry->head_to_index->new_file.path) {
242 fpath = entry->head_to_index->new_file.path;
243 } else if (entry->index_to_workdir && entry->index_to_workdir->new_file.path) {
244 fpath = entry->index_to_workdir->new_file.path;
245 } else if (entry->index_to_workdir && entry->index_to_workdir->old_file.path) {
246 fpath = entry->index_to_workdir->old_file.path;
247 }
248
249 if (fpath) {
250 snprintf(se->path, sizeof(se->path), "%s", fpath);
251 }
252
253 se->flags = 0;
254
255 /* index (staged) flags */
256 if (entry->status & GIT_STATUS_INDEX_NEW) {
258 }
259 if (entry->status & GIT_STATUS_INDEX_MODIFIED) {
261 }
262 if (entry->status & GIT_STATUS_INDEX_DELETED) {
264 }
265
266 /* workdir (unstaged) flags */
267 if (entry->status & GIT_STATUS_WT_NEW) {
268 se->flags |= N_GIT_STATUS_NEW;
269 }
270 if (entry->status & GIT_STATUS_WT_MODIFIED) {
272 }
273 if (entry->status & GIT_STATUS_WT_DELETED) {
275 }
276
278 }
279
280 git_status_list_free(status_list);
281 return list;
282} /* n_git_status */
283
290int n_git_stage(N_GIT_REPO* repo, const char* filepath) {
291 __n_assert(repo, return -1);
292 __n_assert(repo->repo, return -1);
293 __n_assert(filepath, return -1);
294
295 git_index* index = NULL;
296 int ret = -1;
297
298 int err = git_repository_index(&index, repo->repo);
299 if (err < 0) {
300 _n_git_log_error("n_git_stage");
301 return -1;
302 }
303
304 err = git_index_add_bypath(index, filepath);
305 if (err < 0) {
306 _n_git_log_error("n_git_stage");
307 goto cleanup;
308 }
309
310 err = git_index_write(index);
311 if (err < 0) {
312 _n_git_log_error("n_git_stage");
313 goto cleanup;
314 }
315
316 ret = 0;
317
318cleanup:
319 git_index_free(index);
320 return ret;
321} /* n_git_stage */
322
329 __n_assert(repo, return -1);
330 __n_assert(repo->repo, return -1);
331
332 git_index* index = NULL;
333 int ret = -1;
334
335 int err = git_repository_index(&index, repo->repo);
336 if (err < 0) {
337 _n_git_log_error("n_git_stage_all");
338 return -1;
339 }
340
341 err = git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL);
342 if (err < 0) {
343 _n_git_log_error("n_git_stage_all");
344 goto cleanup;
345 }
346
347 err = git_index_write(index);
348 if (err < 0) {
349 _n_git_log_error("n_git_stage_all");
350 goto cleanup;
351 }
352
353 ret = 0;
354
355cleanup:
356 git_index_free(index);
357 return ret;
358} /* n_git_stage_all */
359
366int n_git_unstage(N_GIT_REPO* repo, const char* filepath) {
367 __n_assert(repo, return -1);
368 __n_assert(repo->repo, return -1);
369 __n_assert(filepath, return -1);
370
371 git_reference* head_ref = NULL;
372 git_object* head_obj = NULL;
373 int ret = -1;
374
375 /* try to get HEAD; for initial commit there may be no HEAD */
376 int err = git_repository_head(&head_ref, repo->repo);
377 if (err == 0) {
378 err = git_reference_peel(&head_obj, head_ref, GIT_OBJECT_COMMIT);
379 if (err < 0) {
380 _n_git_log_error("n_git_unstage");
381 goto cleanup;
382 }
383 }
384
385 git_strarray paths;
386 char* path_str = (char*)filepath;
387 paths.strings = &path_str;
388 paths.count = 1;
389
390 err = git_reset_default(repo->repo, head_obj, &paths);
391 if (err < 0) {
392 _n_git_log_error("n_git_unstage");
393 goto cleanup;
394 }
395
396 ret = 0;
397
398cleanup:
399 if (head_obj) git_object_free(head_obj);
400 if (head_ref) git_reference_free(head_ref);
401 return ret;
402} /* n_git_unstage */
403
412int n_git_commit(N_GIT_REPO* repo, const char* message, const char* author_name, const char* author_email) {
413 __n_assert(repo, return -1);
414 __n_assert(repo->repo, return -1);
415 __n_assert(message, return -1);
416 __n_assert(author_name, return -1);
417 __n_assert(author_email, return -1);
418
419 git_index* index = NULL;
420 git_oid tree_oid;
421 git_oid commit_oid;
422 git_tree* tree = NULL;
423 git_signature* sig = NULL;
424 git_commit* parent_commit = NULL;
425 git_reference* head_ref = NULL;
426 int ret = -1;
427
428 int err = git_repository_index(&index, repo->repo);
429 if (err < 0) {
430 _n_git_log_error("n_git_commit");
431 return -1;
432 }
433
434 err = git_index_write_tree(&tree_oid, index);
435 if (err < 0) {
436 _n_git_log_error("n_git_commit");
437 goto cleanup;
438 }
439
440 err = git_tree_lookup(&tree, repo->repo, &tree_oid);
441 if (err < 0) {
442 _n_git_log_error("n_git_commit");
443 goto cleanup;
444 }
445
446 err = git_signature_now(&sig, author_name, author_email);
447 if (err < 0) {
448 _n_git_log_error("n_git_commit");
449 goto cleanup;
450 }
451
452 /* try to get HEAD as parent; if no HEAD this is the initial commit */
453 int have_parent = 0;
454 err = git_repository_head(&head_ref, repo->repo);
455 if (err == 0) {
456 err = git_reference_peel((git_object**)&parent_commit, head_ref, GIT_OBJECT_COMMIT);
457 if (err == 0) {
458 have_parent = 1;
459 }
460 }
461
462 if (have_parent) {
463 const git_commit* parents[] = {parent_commit};
464 err = git_commit_create(&commit_oid, repo->repo, "HEAD", sig, sig, "UTF-8", message, tree, 1, parents);
465 } else {
466 err = git_commit_create(&commit_oid, repo->repo, "HEAD", sig, sig, "UTF-8", message, tree, 0, NULL);
467 }
468
469 if (err < 0) {
470 _n_git_log_error("n_git_commit");
471 goto cleanup;
472 }
473
474 ret = 0;
475
476cleanup:
477 if (parent_commit) git_commit_free(parent_commit);
478 if (head_ref) git_reference_free(head_ref);
479 if (sig) git_signature_free(sig);
480 if (tree) git_tree_free(tree);
481 git_index_free(index);
482 return ret;
483} /* n_git_commit */
484
491LIST* n_git_log(N_GIT_REPO* repo, size_t max_entries) {
492 __n_assert(repo, return NULL);
493 __n_assert(repo->repo, return NULL);
494
495 git_revwalk* walker = NULL;
496 int err = git_revwalk_new(&walker, repo->repo);
497 if (err < 0) {
498 _n_git_log_error("n_git_log");
499 return NULL;
500 }
501
502 git_revwalk_sorting(walker, GIT_SORT_TIME);
503
504 err = git_revwalk_push_head(walker);
505 if (err < 0) {
506 _n_git_log_error("n_git_log");
507 git_revwalk_free(walker);
508 return NULL;
509 }
510
512 if (!list) {
513 git_revwalk_free(walker);
514 return NULL;
515 }
516
517 git_oid oid;
518 size_t count = 0;
519 while (count < max_entries && git_revwalk_next(&oid, walker) == 0) {
520 git_commit* commit = NULL;
521 err = git_commit_lookup(&commit, repo->repo, &oid);
522 if (err < 0) {
523 _n_git_log_error("n_git_log");
524 continue;
525 }
526
527 N_GIT_COMMIT_INFO* info = NULL;
528 Malloc(info, N_GIT_COMMIT_INFO, 1);
529 if (!info) {
530 git_commit_free(commit);
531 continue;
532 }
533
534 /* hash */
535 char hex[GIT_OID_SHA1_HEXSIZE + 1];
536 git_oid_tostr(hex, sizeof(hex), &oid);
537 snprintf(info->hash, sizeof(info->hash), "%s", hex);
538 snprintf(info->short_hash, sizeof(info->short_hash), "%.8s", hex);
539
540 /* message */
541 const char* msg = git_commit_message(commit);
542 if (msg) {
543 snprintf(info->message, sizeof(info->message), "%s", msg);
544 }
545
546 /* author */
547 const git_signature* author = git_commit_author(commit);
548 if (author) {
549 snprintf(info->author, sizeof(info->author), "%s <%s>", author->name ? author->name : "", author->email ? author->email : "");
550 info->timestamp = (int64_t)author->when.time;
551 }
552
554 git_commit_free(commit);
555 count++;
556 }
557
558 git_revwalk_free(walker);
559 return list;
560} /* n_git_log */
561
568 __n_assert(repo, return NULL);
569 __n_assert(repo->repo, return NULL);
570
571 git_diff* diff = NULL;
572 int err = git_diff_index_to_workdir(&diff, repo->repo, NULL, NULL);
573 if (err < 0) {
574 _n_git_log_error("n_git_diff_workdir");
575 return NULL;
576 }
577
578 N_STR* result = new_nstr(256);
579 if (!result) {
580 git_diff_free(diff);
581 return NULL;
582 }
583
584 git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, _n_git_diff_print_cb, &result);
585
586 git_diff_free(diff);
587 return result;
588} /* n_git_diff_workdir */
589
597N_STR* n_git_diff_commits(N_GIT_REPO* repo, const char* hash_a, const char* hash_b) {
598 __n_assert(repo, return NULL);
599 __n_assert(repo->repo, return NULL);
600 __n_assert(hash_a, return NULL);
601 __n_assert(hash_b, return NULL);
602
603 git_object* obj_a = NULL;
604 git_object* obj_b = NULL;
605 git_commit* commit_a = NULL;
606 git_commit* commit_b = NULL;
607 git_tree* tree_a = NULL;
608 git_tree* tree_b = NULL;
609 git_diff* diff = NULL;
610 N_STR* result = NULL;
611 int err = 0;
612
613 err = git_revparse_single(&obj_a, repo->repo, hash_a);
614 if (err < 0) {
615 _n_git_log_error("n_git_diff_commits");
616 goto cleanup;
617 }
618
619 err = git_revparse_single(&obj_b, repo->repo, hash_b);
620 if (err < 0) {
621 _n_git_log_error("n_git_diff_commits");
622 goto cleanup;
623 }
624
625 err = git_commit_lookup(&commit_a, repo->repo, git_object_id(obj_a));
626 if (err < 0) {
627 _n_git_log_error("n_git_diff_commits");
628 goto cleanup;
629 }
630
631 err = git_commit_lookup(&commit_b, repo->repo, git_object_id(obj_b));
632 if (err < 0) {
633 _n_git_log_error("n_git_diff_commits");
634 goto cleanup;
635 }
636
637 err = git_commit_tree(&tree_a, commit_a);
638 if (err < 0) {
639 _n_git_log_error("n_git_diff_commits");
640 goto cleanup;
641 }
642
643 err = git_commit_tree(&tree_b, commit_b);
644 if (err < 0) {
645 _n_git_log_error("n_git_diff_commits");
646 goto cleanup;
647 }
648
649 err = git_diff_tree_to_tree(&diff, repo->repo, tree_a, tree_b, NULL);
650 if (err < 0) {
651 _n_git_log_error("n_git_diff_commits");
652 goto cleanup;
653 }
654
655 result = new_nstr(256);
656 if (!result) {
657 goto cleanup;
658 }
659
660 git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, _n_git_diff_print_cb, &result);
661
662cleanup:
663 if (diff) git_diff_free(diff);
664 if (tree_b) git_tree_free(tree_b);
665 if (tree_a) git_tree_free(tree_a);
666 if (commit_b) git_commit_free(commit_b);
667 if (commit_a) git_commit_free(commit_a);
668 if (obj_b) git_object_free(obj_b);
669 if (obj_a) git_object_free(obj_a);
670 return result;
671} /* n_git_diff_commits */
672
679int n_git_checkout_path(N_GIT_REPO* repo, const char* filepath) {
680 __n_assert(repo, return -1);
681 __n_assert(repo->repo, return -1);
682 __n_assert(filepath, return -1);
683
684 char* path_str = (char*)filepath;
685 git_strarray paths;
686 paths.strings = &path_str;
687 paths.count = 1;
688
689 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
690 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
691 opts.paths = paths;
692
693 int err = git_checkout_head(repo->repo, &opts);
694 if (err < 0) {
695 _n_git_log_error("n_git_checkout_path");
696 return -1;
697 }
698
699 return 0;
700} /* n_git_checkout_path */
701
708int n_git_checkout_commit(N_GIT_REPO* repo, const char* hash) {
709 __n_assert(repo, return -1);
710 __n_assert(repo->repo, return -1);
711 __n_assert(hash, return -1);
712
713 git_object* obj = NULL;
714 int ret = -1;
715
716 int err = git_revparse_single(&obj, repo->repo, hash);
717 if (err < 0) {
718 _n_git_log_error("n_git_checkout_commit");
719 return -1;
720 }
721
722 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
723 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
724
725 err = git_checkout_tree(repo->repo, obj, &opts);
726 if (err < 0) {
727 _n_git_log_error("n_git_checkout_commit");
728 goto cleanup;
729 }
730
731 err = git_repository_set_head_detached(repo->repo, git_object_id(obj));
732 if (err < 0) {
733 _n_git_log_error("n_git_checkout_commit");
734 goto cleanup;
735 }
736
737 ret = 0;
738
739cleanup:
740 git_object_free(obj);
741 return ret;
742} /* n_git_checkout_commit */
743
750 __n_assert(repo, return NULL);
751 __n_assert(repo->repo, return NULL);
752
753 git_branch_iterator* iter = NULL;
754 int err = git_branch_iterator_new(&iter, repo->repo, GIT_BRANCH_LOCAL);
755 if (err < 0) {
756 _n_git_log_error("n_git_list_branches");
757 return NULL;
758 }
759
761 if (!list) {
762 git_branch_iterator_free(iter);
763 return NULL;
764 }
765
766 git_reference* ref = NULL;
767 git_branch_t type;
768 while (git_branch_next(&ref, &type, iter) == 0) {
769 const char* name = NULL;
770 err = git_branch_name(&name, ref);
771 if (err == 0 && name) {
772 char* dup = strdup(name);
773 if (dup) {
775 }
776 }
777 git_reference_free(ref);
778 }
779
780 git_branch_iterator_free(iter);
781 return list;
782} /* n_git_list_branches */
783
790int n_git_create_branch(N_GIT_REPO* repo, const char* branch_name) {
791 __n_assert(repo, return -1);
792 __n_assert(repo->repo, return -1);
793 __n_assert(branch_name, return -1);
794
795 git_reference* head_ref = NULL;
796 git_commit* head_commit = NULL;
797 git_reference* new_branch = NULL;
798 int ret = -1;
799
800 int err = git_repository_head(&head_ref, repo->repo);
801 if (err < 0) {
802 _n_git_log_error("n_git_create_branch");
803 return -1;
804 }
805
806 err = git_reference_peel((git_object**)&head_commit, head_ref, GIT_OBJECT_COMMIT);
807 if (err < 0) {
808 _n_git_log_error("n_git_create_branch");
809 goto cleanup;
810 }
811
812 err = git_branch_create(&new_branch, repo->repo, branch_name, head_commit, 0);
813 if (err < 0) {
814 _n_git_log_error("n_git_create_branch");
815 goto cleanup;
816 }
817
818 ret = 0;
819
820cleanup:
821 if (new_branch) git_reference_free(new_branch);
822 if (head_commit) git_commit_free(head_commit);
823 git_reference_free(head_ref);
824 return ret;
825} /* n_git_create_branch */
826
833int n_git_switch_branch(N_GIT_REPO* repo, const char* branch_name) {
834 __n_assert(repo, return -1);
835 __n_assert(repo->repo, return -1);
836 __n_assert(branch_name, return -1);
837
838 git_reference* branch_ref = NULL;
839 int ret = -1;
840
841 int err = git_branch_lookup(&branch_ref, repo->repo, branch_name, GIT_BRANCH_LOCAL);
842 if (err < 0) {
843 _n_git_log_error("n_git_switch_branch");
844 return -1;
845 }
846
847 char refname[512];
848 snprintf(refname, sizeof(refname), "refs/heads/%s", branch_name);
849
850 err = git_repository_set_head(repo->repo, refname);
851 if (err < 0) {
852 _n_git_log_error("n_git_switch_branch");
853 goto cleanup;
854 }
855
856 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
857 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
858
859 err = git_checkout_head(repo->repo, &opts);
860 if (err < 0) {
861 _n_git_log_error("n_git_switch_branch");
862 goto cleanup;
863 }
864
865 ret = 0;
866
867cleanup:
868 git_reference_free(branch_ref);
869 return ret;
870} /* n_git_switch_branch */
871
879int n_git_current_branch(N_GIT_REPO* repo, char* out, size_t out_size) {
880 __n_assert(repo, return -1);
881 __n_assert(repo->repo, return -1);
882 __n_assert(out, return -1);
883
884 git_reference* head_ref = NULL;
885
886 int err = git_repository_head(&head_ref, repo->repo);
887 if (err < 0) {
888 _n_git_log_error("n_git_current_branch");
889 return -1;
890 }
891
892 const char* shorthand = git_reference_shorthand(head_ref);
893 if (shorthand) {
894 snprintf(out, out_size, "%s", shorthand);
895 } else {
896 out[0] = '\0';
897 }
898
899 git_reference_free(head_ref);
900 return 0;
901} /* n_git_current_branch */
902
909int n_git_delete_branch(N_GIT_REPO* repo, const char* branch_name) {
910 __n_assert(repo, return -1);
911 __n_assert(repo->repo, return -1);
912 __n_assert(branch_name, return -1);
913
914 git_reference* branch_ref = NULL;
915
916 int err = git_branch_lookup(&branch_ref, repo->repo, branch_name, GIT_BRANCH_LOCAL);
917 if (err < 0) {
918 _n_git_log_error("n_git_delete_branch");
919 return -1;
920 }
921
922 err = git_branch_delete(branch_ref);
923 git_reference_free(branch_ref);
924
925 if (err < 0) {
926 _n_git_log_error("n_git_delete_branch");
927 return -1;
928 }
929
930 return 0;
931} /* n_git_delete_branch */
932
933/* ========================================================================
934 * REMOTE OPERATIONS: auth, push, pull
935 * ======================================================================== */
936
940static int _n_git_cred_cb(git_credential** out, const char* url, const char* username_from_url, unsigned int allowed_types, void* payload) {
941 (void)url;
942 N_GIT_REMOTE_AUTH* auth = (N_GIT_REMOTE_AUTH*)payload;
943 if (!auth) return GIT_PASSTHROUGH;
944
945 switch (auth->type) {
946 case N_GIT_AUTH_TOKEN:
947 if ((allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) &&
948 auth->password) {
949 const char* user = auth->username ? auth->username : "x-access-token";
950 return git_credential_userpass_plaintext_new(out, user, auth->password);
951 }
952 break;
953
954 case N_GIT_AUTH_BASIC:
955 if ((allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) &&
956 auth->username && auth->password) {
957 return git_credential_userpass_plaintext_new(out, auth->username, auth->password);
958 }
959 break;
960
961 case N_GIT_AUTH_SSH:
962 if ((allowed_types & GIT_CREDENTIAL_SSH_KEY) &&
963 auth->ssh_privkey_path) {
964 const char* user = auth->username ? auth->username : (username_from_url ? username_from_url : "git");
965 return git_credential_ssh_key_new(out, user,
967 auth->ssh_passphrase ? auth->ssh_passphrase : "");
968 }
969 /* Try SSH agent if key auth not available */
970 if (allowed_types & GIT_CREDENTIAL_SSH_KEY) {
971 const char* user = auth->username ? auth->username : (username_from_url ? username_from_url : "git");
972 return git_credential_ssh_key_from_agent(out, user);
973 }
974 break;
975
976 default:
977 break;
978 }
979
980 return GIT_PASSTHROUGH;
981} /* _n_git_cred_cb */
982
987 __n_assert(repo, return -1);
988 __n_assert(auth, return -1);
989
990 /* Free existing auth */
991 if (repo->remote_auth) {
997 FreeNoLog(repo->remote_auth);
998 }
999
1000 N_GIT_REMOTE_AUTH* a = NULL;
1002 __n_assert(a, return -1);
1003 memset(a, 0, sizeof(*a));
1004
1005 a->type = auth->type;
1006 if (auth->username) a->username = strdup(auth->username);
1007 if (auth->password) a->password = strdup(auth->password);
1008 if (auth->ssh_pubkey_path) a->ssh_pubkey_path = strdup(auth->ssh_pubkey_path);
1009 if (auth->ssh_privkey_path) a->ssh_privkey_path = strdup(auth->ssh_privkey_path);
1010 if (auth->ssh_passphrase) a->ssh_passphrase = strdup(auth->ssh_passphrase);
1011
1012 repo->remote_auth = a;
1013 return 0;
1014} /* n_git_set_remote_auth */
1015
1019int n_git_remote_set_url(N_GIT_REPO* repo, const char* url) {
1020 __n_assert(repo, return -1);
1021 __n_assert(repo->repo, return -1);
1022 __n_assert(url, return -1);
1023
1025
1026 git_remote* remote = NULL;
1027 int err = git_remote_lookup(&remote, repo->repo, "origin");
1028 if (err == 0) {
1029 /* Remote exists — update URL if different */
1030 const char* current_url = git_remote_url(remote);
1031 if (!current_url || strcmp(current_url, url) != 0) {
1032 git_remote_free(remote);
1033 err = git_remote_set_url(repo->repo, "origin", url);
1034 if (err < 0) {
1035 _n_git_log_error("n_git_remote_set_url: set_url");
1036 return -1;
1037 }
1038 } else {
1039 git_remote_free(remote);
1040 }
1041 } else {
1042 /* Remote does not exist — create it */
1043 err = git_remote_create(&remote, repo->repo, "origin", url);
1044 if (err < 0) {
1045 _n_git_log_error("n_git_remote_set_url: create");
1046 return -1;
1047 }
1048 git_remote_free(remote);
1049 }
1050
1051 return 0;
1052} /* n_git_remote_set_url */
1053
1058 __n_assert(repo, return -1);
1059 __n_assert(repo->repo, return -1);
1060
1062
1063 /* Get current branch name */
1064 char branch[128];
1065 if (n_git_current_branch(repo, branch, sizeof(branch)) != 0) {
1066 n_log(LOG_ERR, "n_git_push: cannot determine current branch");
1067 return -1;
1068 }
1069
1070 /* Build refspec: refs/heads/branch:refs/heads/branch */
1071 char refspec[300];
1072 snprintf(refspec, sizeof(refspec), "refs/heads/%s:refs/heads/%s",
1073 branch, branch);
1074
1075 git_remote* remote = NULL;
1076 int err = git_remote_lookup(&remote, repo->repo, "origin");
1077 if (err < 0) {
1078 _n_git_log_error("n_git_push: remote lookup");
1079 return -1;
1080 }
1081
1082 /* Set up push options with credential callback */
1083 git_push_options opts = GIT_PUSH_OPTIONS_INIT;
1084 if (repo->remote_auth) {
1085 opts.callbacks.credentials = _n_git_cred_cb;
1086 opts.callbacks.payload = repo->remote_auth;
1087 }
1088
1089 const char* refspecs[] = {refspec};
1090 git_strarray refspec_arr = {(char**)refspecs, 1};
1091
1092 err = git_remote_push(remote, &refspec_arr, &opts);
1093 git_remote_free(remote);
1094
1095 if (err < 0) {
1096 _n_git_log_error("n_git_push");
1097 return -1;
1098 }
1099
1100 n_log(LOG_INFO, "n_git_push: pushed %s to origin", branch);
1101 return 0;
1102} /* n_git_push */
1103
1108 __n_assert(repo, return -1);
1109 __n_assert(repo->repo, return -1);
1110
1112
1113 /* Fetch from origin */
1114 git_remote* remote = NULL;
1115 int err = git_remote_lookup(&remote, repo->repo, "origin");
1116 if (err < 0) {
1117 _n_git_log_error("n_git_pull: remote lookup");
1118 return -1;
1119 }
1120
1121 git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
1122 if (repo->remote_auth) {
1123 fetch_opts.callbacks.credentials = _n_git_cred_cb;
1124 fetch_opts.callbacks.payload = repo->remote_auth;
1125 }
1126
1127 err = git_remote_fetch(remote, NULL, &fetch_opts, "pull");
1128 git_remote_free(remote);
1129
1130 if (err < 0) {
1131 _n_git_log_error("n_git_pull: fetch");
1132 return -1;
1133 }
1134
1135 /* Get current branch name */
1136 char branch[128];
1137 if (n_git_current_branch(repo, branch, sizeof(branch)) != 0) {
1138 n_log(LOG_ERR, "n_git_pull: cannot determine current branch");
1139 return -1;
1140 }
1141
1142 /* Look up the remote tracking ref: refs/remotes/origin/<branch> */
1143 char remote_ref[300];
1144 snprintf(remote_ref, sizeof(remote_ref), "refs/remotes/origin/%s", branch);
1145
1146 git_oid remote_oid;
1147 err = git_reference_name_to_id(&remote_oid, repo->repo, remote_ref);
1148 if (err < 0) {
1149 _n_git_log_error("n_git_pull: remote ref lookup");
1150 return -1;
1151 }
1152
1153 /* Get HEAD commit */
1154 git_oid local_oid;
1155 err = git_reference_name_to_id(&local_oid, repo->repo, "HEAD");
1156 if (err < 0) {
1157 _n_git_log_error("n_git_pull: HEAD lookup");
1158 return -1;
1159 }
1160
1161 /* Check if already up to date */
1162 if (git_oid_equal(&local_oid, &remote_oid)) {
1163 n_log(LOG_INFO, "n_git_pull: already up to date");
1164 return 0;
1165 }
1166
1167 /* Check if fast-forward is possible */
1168 git_annotated_commit* remote_commit = NULL;
1169 err = git_annotated_commit_lookup(&remote_commit, repo->repo, &remote_oid);
1170 if (err < 0) {
1171 _n_git_log_error("n_git_pull: annotated commit lookup");
1172 return -1;
1173 }
1174
1175 git_merge_analysis_t analysis;
1176 git_merge_preference_t preference;
1177 const git_annotated_commit* their_heads[] = {remote_commit};
1178 err = git_merge_analysis(&analysis, &preference, repo->repo, their_heads, 1);
1179 if (err < 0) {
1180 git_annotated_commit_free(remote_commit);
1181 _n_git_log_error("n_git_pull: merge analysis");
1182 return -1;
1183 }
1184
1185 if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
1186 git_annotated_commit_free(remote_commit);
1187 n_log(LOG_INFO, "n_git_pull: already up to date");
1188 return 0;
1189 }
1190
1191 if (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD) {
1192 /* Fast-forward: move HEAD to remote commit */
1193 git_reference* head_ref = NULL;
1194 err = git_repository_head(&head_ref, repo->repo);
1195 if (err < 0) {
1196 git_annotated_commit_free(remote_commit);
1197 _n_git_log_error("n_git_pull: get HEAD ref");
1198 return -1;
1199 }
1200
1201 git_reference* new_ref = NULL;
1202 err = git_reference_set_target(&new_ref, head_ref, &remote_oid, "pull: fast-forward");
1203 git_reference_free(head_ref);
1204 if (err < 0) {
1205 git_annotated_commit_free(remote_commit);
1206 _n_git_log_error("n_git_pull: set target");
1207 return -1;
1208 }
1209 git_reference_free(new_ref);
1210
1211 /* Checkout the new HEAD */
1212 git_object* target = NULL;
1213 git_object_lookup(&target, repo->repo, &remote_oid, GIT_OBJECT_COMMIT);
1214 if (target) {
1215 git_checkout_options co_opts = GIT_CHECKOUT_OPTIONS_INIT;
1216 co_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
1217 git_checkout_tree(repo->repo, target, &co_opts);
1218 git_object_free(target);
1219 }
1220
1221 git_annotated_commit_free(remote_commit);
1222 n_log(LOG_INFO, "n_git_pull: fast-forwarded %s", branch);
1223 return 0;
1224 }
1225
1226 /* Not a fast-forward — we don't handle merge conflicts */
1227 git_annotated_commit_free(remote_commit);
1228 n_log(LOG_ERR, "n_git_pull: cannot fast-forward, manual merge required");
1229 return -1;
1230} /* n_git_pull */
1231
1232#pragma GCC diagnostic pop
1233
1234#endif /* HAVE_LIBGIT2 */
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:271
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:203
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:278
char short_hash[10]
short (abbreviated) hex hash
Definition n_git.h:102
char author[256]
author name and email
Definition n_git.h:106
int64_t timestamp
unix timestamp of the commit
Definition n_git.h:108
char message[512]
first line of commit message
Definition n_git.h:104
char * password
password or personal access token, or NULL
Definition n_git.h:70
char * ssh_privkey_path
path to SSH private key file, or NULL
Definition n_git.h:74
char hash[42]
full hex hash
Definition n_git.h:100
char * ssh_pubkey_path
path to SSH public key file, or NULL (derived from private key + ".pub")
Definition n_git.h:72
int flags
combination of N_GIT_STATUS_* flags
Definition n_git.h:94
char * path
filesystem path to the repository
Definition n_git.h:84
git_repository * repo
libgit2 repository handle
Definition n_git.h:82
char * ssh_passphrase
passphrase for SSH key, or NULL
Definition n_git.h:76
int type
auth type: N_GIT_AUTH_NONE/TOKEN/BASIC/SSH
Definition n_git.h:66
char * username
username for basic auth or SSH, or NULL
Definition n_git.h:68
char path[512]
relative path of the file
Definition n_git.h:92
N_GIT_REMOTE_AUTH * remote_auth
credentials for remote operations, or NULL
Definition n_git.h:86
void n_git_close(N_GIT_REPO **repo)
Close a Git repository and free resources.
Definition n_git.c:185
#define N_GIT_AUTH_TOKEN
Remote auth: personal access token (used as password with empty or "x-access-token" user)
Definition n_git.h:57
N_STR * n_git_diff_commits(N_GIT_REPO *repo, const char *hash_a, const char *hash_b)
Get a diff between two commits identified by hash.
Definition n_git.c:597
#define N_GIT_STATUS_NEW
File is new / untracked.
Definition n_git.h:46
int n_git_push(N_GIT_REPO *repo)
Push the current branch to the "origin" remote.
Definition n_git.c:1057
int n_git_remote_set_url(N_GIT_REPO *repo, const char *url)
Ensure a remote named "origin" exists with the given URL.
Definition n_git.c:1019
int n_git_switch_branch(N_GIT_REPO *repo, const char *branch_name)
Switch to an existing local branch.
Definition n_git.c:833
int n_git_pull(N_GIT_REPO *repo)
Fetch from "origin" and fast-forward merge the current branch.
Definition n_git.c:1107
int n_git_checkout_path(N_GIT_REPO *repo, const char *filepath)
Restore a single file from HEAD (discard working directory changes).
Definition n_git.c:679
#define N_GIT_STATUS_MODIFIED
File has been modified.
Definition n_git.h:48
LIST * n_git_list_branches(N_GIT_REPO *repo)
List all local branch names.
Definition n_git.c:749
LIST * n_git_status(N_GIT_REPO *repo)
Get the status of all files in the working directory.
Definition n_git.c:210
int n_git_unstage(N_GIT_REPO *repo, const char *filepath)
Unstage a single file (reset from HEAD).
Definition n_git.c:366
int n_git_set_remote_auth(N_GIT_REPO *repo, const N_GIT_REMOTE_AUTH *auth)
Set remote authentication credentials on a repo handle.
Definition n_git.c:986
LIST * n_git_log(N_GIT_REPO *repo, size_t max_entries)
Retrieve commit log entries starting from HEAD.
Definition n_git.c:491
#define N_GIT_STATUS_STAGED
File is staged in the index.
Definition n_git.h:52
#define N_GIT_AUTH_SSH
Remote auth: SSH key file.
Definition n_git.h:61
#define N_GIT_AUTH_BASIC
Remote auth: username + password.
Definition n_git.h:59
int n_git_stage_all(N_GIT_REPO *repo)
Stage all modified and untracked files.
Definition n_git.c:328
int n_git_stage(N_GIT_REPO *repo, const char *filepath)
Stage a single file by path.
Definition n_git.c:290
int n_git_create_branch(N_GIT_REPO *repo, const char *branch_name)
Create a new branch from HEAD.
Definition n_git.c:790
int n_git_current_branch(N_GIT_REPO *repo, char *out, size_t out_size)
Get the name of the current branch.
Definition n_git.c:879
int n_git_checkout_commit(N_GIT_REPO *repo, const char *hash)
Checkout a specific commit (detached HEAD).
Definition n_git.c:708
#define N_GIT_STATUS_DELETED
File has been deleted.
Definition n_git.h:50
int n_git_commit(N_GIT_REPO *repo, const char *message, const char *author_name, const char *author_email)
Create a commit from the current index.
Definition n_git.c:412
N_GIT_REPO * n_git_open(const char *path)
Open an existing Git repository.
Definition n_git.c:123
N_STR * n_git_diff_workdir(N_GIT_REPO *repo)
Get a diff of the working directory against the index.
Definition n_git.c:567
N_GIT_REPO * n_git_init(const char *path)
Initialize a new Git repository.
Definition n_git.c:155
int n_git_delete_branch(N_GIT_REPO *repo, const char *branch_name)
Delete a local branch.
Definition n_git.c:909
Commit metadata.
Definition n_git.h:98
Credentials for remote operations.
Definition n_git.h:64
Wrapper around a git_repository handle.
Definition n_git.h:80
Single file status entry.
Definition n_git.h:90
#define UNLIMITED_LIST_ITEMS
flag to pass to new_generic_list for an unlimited number of item in the list.
Definition n_list.h:72
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:227
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:36
Structure of a generic LIST container.
Definition n_list.h:58
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:88
#define LOG_ERR
error conditions
Definition n_log.h:75
#define LOG_INFO
informational
Definition n_log.h:81
#define nstrprintf_cat(__nstr_var, __format,...)
Macro to quickly allocate and sprintf and cat to a N_STR.
Definition n_str.h:119
N_STR * new_nstr(NSTRBYTE size)
create a new N_STR string
Definition n_str.c:206
A box including a string and his lenght.
Definition n_str.h:60
Common headers and low-level functions & define.
static int _n_git_cred_cb(git_credential **out, const char *url, const char *username_from_url, unsigned int allowed_types, void *payload)
libgit2 credential callback for remote operations.
Definition n_git.c:940
static void _n_git_status_entry_destroy(void *ptr)
Destructor for N_GIT_STATUS_ENTRY used by LIST.
Definition n_git.c:70
static void _n_git_string_destroy(void *ptr)
Destructor for char* branch name strings used by LIST.
Definition n_git.c:88
static void _n_git_log_error(const char *func_name)
Log the last libgit2 error.
Definition n_git.c:57
static void _n_git_ensure_init(void)
Ensure libgit2 is initialized (idempotent).
Definition n_git.c:46
static void _n_git_commit_info_destroy(void *ptr)
Destructor for N_GIT_COMMIT_INFO used by LIST.
Definition n_git.c:79
static int _n_git_diff_print_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload)
Callback for git_diff_print, appends each line to an N_STR.
Definition n_git.c:101
static int _n_git_initialized
static flag ensuring git_libgit2_init is called only once
Definition n_git.c:41
libgit2 wrapper for Git repository operations
Generic log system.