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 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
28#ifdef HAVE_LIBGIT2
29
30/* libgit2 *_INIT macros trigger -Wmissing-field-initializers; suppress here */
31#pragma GCC diagnostic push
32#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
33
34#include "nilorea/n_git.h"
35#include "nilorea/n_log.h"
36#include "nilorea/n_common.h"
37
38#include <string.h>
39#include <stdio.h>
40#include <stdarg.h>
41
42/* GIT_OID_SHA1_HEXSIZE was introduced in libgit2 1.4; older releases (e.g.
43 * the 1.1 shipped by Debian 11) only expose the now-deprecated GIT_OID_HEXSZ.
44 * Both denote the 40-char hex length of a SHA-1 oid. Alias so the code builds
45 * against either. */
46#ifndef GIT_OID_SHA1_HEXSIZE
47#define GIT_OID_SHA1_HEXSIZE GIT_OID_HEXSZ
48#endif
49
51static int _n_git_initialized = 0;
52
56static void _n_git_ensure_init(void) {
57 if (!_n_git_initialized) {
58 git_libgit2_init();
59#if LIBGIT2_VER_MAJOR > 1 || (LIBGIT2_VER_MAJOR == 1 && (LIBGIT2_VER_MINOR > 4 || (LIBGIT2_VER_MINOR == 4 && LIBGIT2_VER_REVISION >= 3)))
60 /* Disable ownership validation, on Windows, NTFS ACLs often
61 * cause false-positive "not owned by current user" errors for
62 * directories like %APPDATA% that legitimately belong to the
63 * user but were created by the system or an installer. */
64 git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0);
65#endif
67 }
68}
69
74static void _n_git_log_error(const char* func_name) {
75 const git_error* err = git_error_last();
76 if (err) {
77 n_log(LOG_ERR, "%s: %s", func_name, err->message);
78 } else {
79 n_log(LOG_ERR, "%s: unknown libgit2 error", func_name);
80 }
81}
82
88#if defined(__MINGW32__) || defined(__MINGW64__)
89static void _n_git_set_status(N_GIT_REPO* repo, const char* fmt, ...) __attribute__((format(__MINGW_PRINTF_FORMAT, 2, 3)));
90#elif defined(__GNUC__) || defined(__clang__)
91static void _n_git_set_status(N_GIT_REPO* repo, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
92#endif
93static void _n_git_set_status(N_GIT_REPO* repo, const char* fmt, ...) {
94 if (!repo) return;
95 repo->last_status[0] = '\0';
96 va_list args;
97 va_start(args, fmt);
98 int ret = vsnprintf(repo->last_status, sizeof(repo->last_status), fmt, args);
99 va_end(args);
100 if (ret < 0) {
101 repo->last_status[0] = '\0';
102 }
103 repo->last_status[sizeof(repo->last_status) - 1] = '\0';
104}
105
110static void _n_git_status_entry_destroy(void* ptr) {
111 // cppcheck-suppress uselessAssignmentPtrArg ; FreeNoLog nulls local copy, intentional for destroy callback API
112 FreeNoLog(ptr);
113}
114
119static void _n_git_commit_info_destroy(void* ptr) {
120 // cppcheck-suppress uselessAssignmentPtrArg ; FreeNoLog nulls local copy, intentional for destroy callback API
121 FreeNoLog(ptr);
122}
123
128static void _n_git_string_destroy(void* ptr) {
129 // cppcheck-suppress uselessAssignmentPtrArg ; FreeNoLog nulls local copy, intentional for destroy callback API
130 FreeNoLog(ptr);
131}
132
141static int _n_git_diff_print_cb(const git_diff_delta* delta, const git_diff_hunk* hunk, const git_diff_line* line, void* payload) {
142 (void)delta;
143 (void)hunk;
144 N_STR** nstr = (N_STR**)payload;
145
146 /* line->content is not null-terminated; copy it */
147 if (line->content && line->content_len > 0) {
148 char* tmp = NULL;
149 Malloc(tmp, char, line->content_len + 2);
150 if (tmp) {
151 memcpy(tmp, line->content, line->content_len);
152 tmp[line->content_len] = '\0';
153 nstrprintf_cat(*nstr, "%c%s", line->origin, tmp);
154 FreeNoLog(tmp);
155 }
156 }
157 return 0;
158}
159
163N_GIT_REPO* n_git_open(const char* path) {
164 __n_assert(path, return NULL);
165
167
168 N_GIT_REPO* ngit = NULL;
169 Malloc(ngit, N_GIT_REPO, 1);
170 __n_assert(ngit, return NULL);
171
172 ngit->path = strdup(path);
173 if (!ngit->path) {
174 n_log(LOG_ERR, "n_git_open: strdup failed");
175 FreeNoLog(ngit);
176 return NULL;
177 }
178
179 int err = git_repository_open(&ngit->repo, path);
180 if (err < 0) {
181 _n_git_log_error("n_git_open");
182 FreeNoLog(ngit->path);
183 FreeNoLog(ngit);
184 return NULL;
185 }
186
187 return ngit;
188} /* n_git_open */
189
195N_GIT_REPO* n_git_init(const char* path) {
196 return n_git_init_ex(path, NULL);
197} /* n_git_init */
198
205N_GIT_REPO* n_git_init_ex(const char* path, const char* initial_branch) {
206 __n_assert(path, return NULL);
207
209
210 N_GIT_REPO* ngit = NULL;
211 Malloc(ngit, N_GIT_REPO, 1);
212 __n_assert(ngit, return NULL);
213
214 ngit->path = strdup(path);
215 if (!ngit->path) {
216 n_log(LOG_ERR, "n_git_init_ex: strdup failed");
217 FreeNoLog(ngit);
218 return NULL;
219 }
220
221 git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
222 opts.flags = GIT_REPOSITORY_INIT_MKPATH;
223 if (initial_branch && initial_branch[0]) {
224 opts.initial_head = initial_branch;
225 }
226
227 int err = git_repository_init_ext(&ngit->repo, path, &opts);
228 if (err < 0) {
229 _n_git_log_error("n_git_init_ex");
230 FreeNoLog(ngit->path);
231 FreeNoLog(ngit);
232 return NULL;
233 }
234
235 return ngit;
236} /* n_git_init_ex */
237
242 __n_assert(repo, return);
243 __n_assert(*repo, return);
244
245 if ((*repo)->repo) {
246 git_repository_free((*repo)->repo);
247 (*repo)->repo = NULL;
248 }
249 FreeNoLog((*repo)->path);
250 if ((*repo)->remote_auth) {
251 FreeNoLog((*repo)->remote_auth->username);
252 FreeNoLog((*repo)->remote_auth->password);
253 FreeNoLog((*repo)->remote_auth->ssh_pubkey_path);
254 FreeNoLog((*repo)->remote_auth->ssh_privkey_path);
255 FreeNoLog((*repo)->remote_auth->ssh_passphrase);
256 FreeNoLog((*repo)->remote_auth);
257 }
258 FreeNoLog(*repo);
259} /* n_git_close */
260
267 __n_assert(repo, return NULL);
268 __n_assert(repo->repo, return NULL);
269
270 git_status_options opts = GIT_STATUS_OPTIONS_INIT;
271 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
272 opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX;
273
274 git_status_list* status_list = NULL;
275 int err = git_status_list_new(&status_list, repo->repo, &opts);
276 if (err < 0) {
277 _n_git_log_error("n_git_status");
278 return NULL;
279 }
280
282 if (!list) {
283 git_status_list_free(status_list);
284 return NULL;
285 }
286
287 size_t count = git_status_list_entrycount(status_list);
288 for (size_t i = 0; i < count; i++) {
289 const git_status_entry* entry = git_status_byindex(status_list, i);
290 if (!entry) continue;
291
292 N_GIT_STATUS_ENTRY* se = NULL;
294 if (!se) continue;
295
296 const char* fpath = NULL;
297 if (entry->head_to_index && entry->head_to_index->new_file.path) {
298 fpath = entry->head_to_index->new_file.path;
299 } else if (entry->index_to_workdir && entry->index_to_workdir->new_file.path) {
300 fpath = entry->index_to_workdir->new_file.path;
301 } else if (entry->index_to_workdir && entry->index_to_workdir->old_file.path) {
302 fpath = entry->index_to_workdir->old_file.path;
303 }
304
305 if (fpath) {
306 snprintf(se->path, sizeof(se->path), "%s", fpath);
307 }
308
309 se->flags = 0;
310
311 /* index (staged) flags */
312 if (entry->status & GIT_STATUS_INDEX_NEW) {
314 }
315 if (entry->status & GIT_STATUS_INDEX_MODIFIED) {
317 }
318 if (entry->status & GIT_STATUS_INDEX_DELETED) {
320 }
321
322 /* workdir (unstaged) flags */
323 if (entry->status & GIT_STATUS_WT_NEW) {
324 se->flags |= N_GIT_STATUS_NEW;
325 }
326 if (entry->status & GIT_STATUS_WT_MODIFIED) {
328 }
329 if (entry->status & GIT_STATUS_WT_DELETED) {
331 }
332
334 }
335
336 git_status_list_free(status_list);
337 return list;
338} /* n_git_status */
339
354int n_git_stage(N_GIT_REPO* repo, const char* filepath) {
355 __n_assert(repo, return -1);
356 __n_assert(repo->repo, return -1);
357 __n_assert(filepath, return -1);
358
359 git_index* index = NULL;
360 int ret = -1;
361
362 int err = git_repository_index(&index, repo->repo);
363 if (err < 0) {
364 _n_git_log_error("n_git_stage");
365 _n_git_set_status(repo, "stage failed: %s", filepath);
366 return -1;
367 }
368
369 size_t len = strlen(filepath);
370
371 /* directory paths (ending with '/') cannot be staged with
372 * git_index_add_bypath; use git_index_add_all with a pathspec instead.
373 * Only '/' is checked because libgit2 uses forward slashes on all
374 * platforms; on POSIX '\\' is a valid filename character. */
375 if (len > 0 && filepath[len - 1] == '/') {
376 char* pathspec_str = (char*)filepath;
377 git_strarray pathspec;
378 pathspec.strings = &pathspec_str;
379 pathspec.count = 1;
380
381 err = git_index_add_all(index, &pathspec, GIT_INDEX_ADD_DEFAULT, NULL, NULL);
382 if (err < 0) {
383 _n_git_log_error("n_git_stage");
384 _n_git_set_status(repo, "stage failed: %s", filepath);
385 goto cleanup;
386 }
387 } else {
388 err = git_index_add_bypath(index, filepath);
389 if (err < 0) {
390 _n_git_log_error("n_git_stage");
391 _n_git_set_status(repo, "stage failed: %s", filepath);
392 goto cleanup;
393 }
394 }
395
396 err = git_index_write(index);
397 if (err < 0) {
398 _n_git_log_error("n_git_stage");
399 _n_git_set_status(repo, "stage failed: %s", filepath);
400 goto cleanup;
401 }
402
403 _n_git_set_status(repo, "staged: %s", filepath);
404 ret = 0;
405
406cleanup:
407 git_index_free(index);
408 return ret;
409} /* n_git_stage */
410
417 __n_assert(repo, return -1);
418 __n_assert(repo->repo, return -1);
419
420 git_index* index = NULL;
421 int ret = -1;
422
423 int err = git_repository_index(&index, repo->repo);
424 if (err < 0) {
425 _n_git_log_error("n_git_stage_all");
426 _n_git_set_status(repo, "stage all failed");
427 return -1;
428 }
429
430 err = git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL);
431 if (err < 0) {
432 _n_git_log_error("n_git_stage_all");
433 _n_git_set_status(repo, "stage all failed");
434 goto cleanup;
435 }
436
437 err = git_index_write(index);
438 if (err < 0) {
439 _n_git_log_error("n_git_stage_all");
440 _n_git_set_status(repo, "stage all failed");
441 goto cleanup;
442 }
443
444 _n_git_set_status(repo, "all files staged");
445 ret = 0;
446
447cleanup:
448 git_index_free(index);
449 return ret;
450} /* n_git_stage_all */
451
458int n_git_unstage(N_GIT_REPO* repo, const char* filepath) {
459 __n_assert(repo, return -1);
460 __n_assert(repo->repo, return -1);
461 __n_assert(filepath, return -1);
462
463 git_reference* head_ref = NULL;
464 git_object* head_obj = NULL;
465 int ret = -1;
466
467 /* try to get HEAD; for initial commit there may be no HEAD */
468 int err = git_repository_head(&head_ref, repo->repo);
469 if (err == 0) {
470 err = git_reference_peel(&head_obj, head_ref, GIT_OBJECT_COMMIT);
471 if (err < 0) {
472 _n_git_log_error("n_git_unstage");
473 _n_git_set_status(repo, "unstage failed: %s", filepath);
474 goto cleanup;
475 }
476 }
477
478 git_strarray paths;
479 char* path_str = (char*)filepath;
480 paths.strings = &path_str;
481 paths.count = 1;
482
483 err = git_reset_default(repo->repo, head_obj, &paths);
484 if (err < 0) {
485 _n_git_log_error("n_git_unstage");
486 _n_git_set_status(repo, "unstage failed: %s", filepath);
487 goto cleanup;
488 }
489
490 _n_git_set_status(repo, "unstaged: %s", filepath);
491 ret = 0;
492
493cleanup:
494 if (head_obj) git_object_free(head_obj);
495 if (head_ref) git_reference_free(head_ref);
496 return ret;
497} /* n_git_unstage */
498
507int n_git_commit(N_GIT_REPO* repo, const char* message, const char* author_name, const char* author_email) {
508 __n_assert(repo, return -1);
509 __n_assert(repo->repo, return -1);
510 __n_assert(message, return -1);
511 __n_assert(author_name, return -1);
512 __n_assert(author_email, return -1);
513
514 git_index* index = NULL;
515 git_oid tree_oid;
516 git_oid commit_oid;
517 git_tree* tree = NULL;
518 git_signature* sig = NULL;
519 git_commit* parent_commit = NULL;
520 git_reference* head_ref = NULL;
521 int ret = -1;
522
523 int err = git_repository_index(&index, repo->repo);
524 if (err < 0) {
525 _n_git_log_error("n_git_commit");
526 _n_git_set_status(repo, "commit failed: cannot open index");
527 return -1;
528 }
529
530 err = git_index_write_tree(&tree_oid, index);
531 if (err < 0) {
532 _n_git_log_error("n_git_commit");
533 _n_git_set_status(repo, "commit failed: cannot write tree");
534 goto cleanup;
535 }
536
537 err = git_tree_lookup(&tree, repo->repo, &tree_oid);
538 if (err < 0) {
539 _n_git_log_error("n_git_commit");
540 _n_git_set_status(repo, "commit failed: cannot lookup tree");
541 goto cleanup;
542 }
543
544 err = git_signature_now(&sig, author_name, author_email);
545 if (err < 0) {
546 _n_git_log_error("n_git_commit");
547 _n_git_set_status(repo, "commit failed: cannot create signature");
548 goto cleanup;
549 }
550
551 /* try to get HEAD as parent; if no HEAD this is the initial commit */
552 int have_parent = 0;
553 err = git_repository_head(&head_ref, repo->repo);
554 if (err == 0) {
555 err = git_reference_peel((git_object**)&parent_commit, head_ref, GIT_OBJECT_COMMIT);
556 if (err == 0) {
557 have_parent = 1;
558 }
559 }
560
561 if (have_parent) {
562 const git_commit* parents[] = {parent_commit};
563 err = git_commit_create(&commit_oid, repo->repo, "HEAD", sig, sig, "UTF-8", message, tree, 1, parents);
564 } else {
565 err = git_commit_create(&commit_oid, repo->repo, "HEAD", sig, sig, "UTF-8", message, tree, 0, NULL);
566 }
567
568 if (err < 0) {
569 _n_git_log_error("n_git_commit");
570 _n_git_set_status(repo, "commit failed");
571 goto cleanup;
572 }
573
574 {
575 char hex[GIT_OID_SHA1_HEXSIZE + 1];
576 git_oid_tostr(hex, sizeof(hex), &commit_oid);
577 _n_git_set_status(repo, "committed %.8s: %s", hex, message);
578 }
579 ret = 0;
580
581cleanup:
582 if (parent_commit) git_commit_free(parent_commit);
583 if (head_ref) git_reference_free(head_ref);
584 if (sig) git_signature_free(sig);
585 if (tree) git_tree_free(tree);
586 git_index_free(index);
587 return ret;
588} /* n_git_commit */
589
596LIST* n_git_log(N_GIT_REPO* repo, size_t max_entries) {
597 __n_assert(repo, return NULL);
598 __n_assert(repo->repo, return NULL);
599
600 git_revwalk* walker = NULL;
601 int err = git_revwalk_new(&walker, repo->repo);
602 if (err < 0) {
603 _n_git_log_error("n_git_log");
604 return NULL;
605 }
606
607 git_revwalk_sorting(walker, GIT_SORT_TIME);
608
609 err = git_revwalk_push_head(walker);
610 if (err < 0) {
611 /* Unborn HEAD (empty repo, no commits), return an empty list.
612 * git_repository_head_unborn() returns:
613 * 1 => unborn HEAD
614 * 0 => not unborn
615 * <0 => error
616 */
617 git_revwalk_free(walker);
618 int unborn = git_repository_head_unborn(repo->repo);
619 if (unborn == 1) {
621 }
622 if (unborn < 0) {
623 _n_git_log_error("n_git_log");
624 }
625 return NULL;
626 }
627
629 if (!list) {
630 git_revwalk_free(walker);
631 return NULL;
632 }
633
634 git_oid oid;
635 size_t count = 0;
636 while (count < max_entries && git_revwalk_next(&oid, walker) == 0) {
637 git_commit* commit = NULL;
638 err = git_commit_lookup(&commit, repo->repo, &oid);
639 if (err < 0) {
640 _n_git_log_error("n_git_log");
641 continue;
642 }
643
644 N_GIT_COMMIT_INFO* info = NULL;
645 Malloc(info, N_GIT_COMMIT_INFO, 1);
646 if (!info) {
647 git_commit_free(commit);
648 continue;
649 }
650
651 /* hash */
652 char hex[GIT_OID_SHA1_HEXSIZE + 1];
653 git_oid_tostr(hex, sizeof(hex), &oid);
654 snprintf(info->hash, sizeof(info->hash), "%s", hex);
655 snprintf(info->short_hash, sizeof(info->short_hash), "%.8s", hex);
656
657 /* message */
658 const char* msg = git_commit_message(commit);
659 if (msg) {
660 snprintf(info->message, sizeof(info->message), "%s", msg);
661 }
662
663 /* author */
664 const git_signature* author = git_commit_author(commit);
665 if (author) {
666 snprintf(info->author, sizeof(info->author), "%s <%s>", author->name ? author->name : "", author->email ? author->email : "");
667 info->timestamp = (int64_t)author->when.time;
668 }
669
671 git_commit_free(commit);
672 count++;
673 }
674
675 git_revwalk_free(walker);
676 return list;
677} /* n_git_log */
678
685 __n_assert(repo, return NULL);
686 __n_assert(repo->repo, return NULL);
687
688 git_diff* diff = NULL;
689 int err = git_diff_index_to_workdir(&diff, repo->repo, NULL, NULL);
690 if (err < 0) {
691 _n_git_log_error("n_git_diff_workdir");
692 return NULL;
693 }
694
695 N_STR* result = new_nstr(256);
696 if (!result) {
697 git_diff_free(diff);
698 return NULL;
699 }
700
701 git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, _n_git_diff_print_cb, &result);
702
703 git_diff_free(diff);
704 return result;
705} /* n_git_diff_workdir */
706
714N_STR* n_git_diff_commits(N_GIT_REPO* repo, const char* hash_a, const char* hash_b) {
715 __n_assert(repo, return NULL);
716 __n_assert(repo->repo, return NULL);
717 __n_assert(hash_a, return NULL);
718 __n_assert(hash_b, return NULL);
719
720 git_object* obj_a = NULL;
721 git_object* obj_b = NULL;
722 git_commit* commit_a = NULL;
723 git_commit* commit_b = NULL;
724 git_tree* tree_a = NULL;
725 git_tree* tree_b = NULL;
726 git_diff* diff = NULL;
727 N_STR* result = NULL;
728 int err = 0;
729
730 err = git_revparse_single(&obj_a, repo->repo, hash_a);
731 if (err < 0) {
732 _n_git_log_error("n_git_diff_commits");
733 goto cleanup;
734 }
735
736 err = git_revparse_single(&obj_b, repo->repo, hash_b);
737 if (err < 0) {
738 _n_git_log_error("n_git_diff_commits");
739 goto cleanup;
740 }
741
742 err = git_commit_lookup(&commit_a, repo->repo, git_object_id(obj_a));
743 if (err < 0) {
744 _n_git_log_error("n_git_diff_commits");
745 goto cleanup;
746 }
747
748 err = git_commit_lookup(&commit_b, repo->repo, git_object_id(obj_b));
749 if (err < 0) {
750 _n_git_log_error("n_git_diff_commits");
751 goto cleanup;
752 }
753
754 err = git_commit_tree(&tree_a, commit_a);
755 if (err < 0) {
756 _n_git_log_error("n_git_diff_commits");
757 goto cleanup;
758 }
759
760 err = git_commit_tree(&tree_b, commit_b);
761 if (err < 0) {
762 _n_git_log_error("n_git_diff_commits");
763 goto cleanup;
764 }
765
766 err = git_diff_tree_to_tree(&diff, repo->repo, tree_a, tree_b, NULL);
767 if (err < 0) {
768 _n_git_log_error("n_git_diff_commits");
769 goto cleanup;
770 }
771
772 result = new_nstr(256);
773 if (!result) {
774 goto cleanup;
775 }
776
777 git_diff_print(diff, GIT_DIFF_FORMAT_PATCH, _n_git_diff_print_cb, &result);
778
779cleanup:
780 if (diff) git_diff_free(diff);
781 if (tree_b) git_tree_free(tree_b);
782 if (tree_a) git_tree_free(tree_a);
783 if (commit_b) git_commit_free(commit_b);
784 if (commit_a) git_commit_free(commit_a);
785 if (obj_b) git_object_free(obj_b);
786 if (obj_a) git_object_free(obj_a);
787 return result;
788} /* n_git_diff_commits */
789
796int n_git_checkout_path(N_GIT_REPO* repo, const char* filepath) {
797 __n_assert(repo, return -1);
798 __n_assert(repo->repo, return -1);
799 __n_assert(filepath, return -1);
800
801 char* path_str = (char*)filepath;
802 git_strarray paths;
803 paths.strings = &path_str;
804 paths.count = 1;
805
806 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
807 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
808 opts.paths = paths;
809
810 int err = git_checkout_head(repo->repo, &opts);
811 if (err < 0) {
812 _n_git_log_error("n_git_checkout_path");
813 _n_git_set_status(repo, "restore failed: %s", filepath);
814 return -1;
815 }
816
817 _n_git_set_status(repo, "restored: %s", filepath);
818 return 0;
819} /* n_git_checkout_path */
820
827int n_git_checkout_commit(N_GIT_REPO* repo, const char* hash) {
828 __n_assert(repo, return -1);
829 __n_assert(repo->repo, return -1);
830 __n_assert(hash, return -1);
831
832 git_object* obj = NULL;
833 int ret = -1;
834
835 int err = git_revparse_single(&obj, repo->repo, hash);
836 if (err < 0) {
837 _n_git_log_error("n_git_checkout_commit");
838 return -1;
839 }
840
841 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
842 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
843
844 err = git_checkout_tree(repo->repo, obj, &opts);
845 if (err < 0) {
846 _n_git_log_error("n_git_checkout_commit");
847 goto cleanup;
848 }
849
850 err = git_repository_set_head_detached(repo->repo, git_object_id(obj));
851 if (err < 0) {
852 _n_git_log_error("n_git_checkout_commit");
853 goto cleanup;
854 }
855
856 ret = 0;
857
858cleanup:
859 git_object_free(obj);
860 return ret;
861} /* n_git_checkout_commit */
862
869 __n_assert(repo, return NULL);
870 __n_assert(repo->repo, return NULL);
871
872 git_branch_iterator* iter = NULL;
873 int err = git_branch_iterator_new(&iter, repo->repo, GIT_BRANCH_LOCAL);
874 if (err < 0) {
875 _n_git_log_error("n_git_list_branches");
876 return NULL;
877 }
878
880 if (!list) {
881 git_branch_iterator_free(iter);
882 return NULL;
883 }
884
885 git_reference* ref = NULL;
886 git_branch_t type;
887 while (git_branch_next(&ref, &type, iter) == 0) {
888 const char* name = NULL;
889 err = git_branch_name(&name, ref);
890 if (err == 0 && name) {
891 char* dup = strdup(name);
892 if (dup) {
894 }
895 }
896 git_reference_free(ref);
897 }
898
899 git_branch_iterator_free(iter);
900 return list;
901} /* n_git_list_branches */
902
911int n_git_create_branch(N_GIT_REPO* repo, const char* branch_name) {
912 __n_assert(repo, return -1);
913 __n_assert(repo->repo, return -1);
914 __n_assert(branch_name, return -1);
915
916 git_reference* head_ref = NULL;
917 git_commit* head_commit = NULL;
918 git_reference* new_branch = NULL;
919 int ret = -1;
920
921 int err = git_repository_head(&head_ref, repo->repo);
922 if (err == GIT_EUNBORNBRANCH) {
923 /* Empty repository (no commits yet). We cannot create a real branch
924 * ref because there is no commit to point to. Instead, update the
925 * symbolic HEAD to target the requested branch, this is equivalent
926 * to `git checkout -b <branch>` on an empty repo. */
927 char refname[512];
928 int n = snprintf(refname, sizeof(refname), "refs/heads/%s", branch_name);
929 if (n < 0 || (size_t)n >= sizeof(refname)) {
930 n_log(LOG_ERR, "n_git_create_branch: branch name too long");
931 return -1;
932 }
933 git_reference* new_ref = NULL;
934 int sym_err = git_reference_symbolic_create(&new_ref, repo->repo, "HEAD", refname, 1, "n_git_create_branch: set HEAD on empty repo");
935 if (sym_err < 0) {
936 _n_git_log_error("n_git_create_branch (unborn HEAD)");
937 return -1;
938 }
939 git_reference_free(new_ref);
940 n_log(LOG_INFO, "n_git_create_branch: set HEAD to '%s' on empty repository", branch_name);
941 _n_git_set_status(repo, "created branch '%s' (empty repo)", branch_name);
942 return 0;
943 }
944 if (err < 0) {
945 _n_git_log_error("n_git_create_branch");
946 _n_git_set_status(repo, "create branch '%s' failed", branch_name);
947 return -1;
948 }
949
950 err = git_reference_peel((git_object**)&head_commit, head_ref, GIT_OBJECT_COMMIT);
951 if (err < 0) {
952 _n_git_log_error("n_git_create_branch");
953 _n_git_set_status(repo, "create branch '%s' failed", branch_name);
954 goto cleanup;
955 }
956
957 err = git_branch_create(&new_branch, repo->repo, branch_name, head_commit, 0);
958 if (err < 0) {
959 _n_git_log_error("n_git_create_branch");
960 _n_git_set_status(repo, "create branch '%s' failed", branch_name);
961 goto cleanup;
962 }
963
964 _n_git_set_status(repo, "created branch '%s'", branch_name);
965 ret = 0;
966
967cleanup:
968 if (new_branch) git_reference_free(new_branch);
969 if (head_commit) git_commit_free(head_commit);
970 git_reference_free(head_ref);
971 return ret;
972} /* n_git_create_branch */
973
980int n_git_switch_branch(N_GIT_REPO* repo, const char* branch_name) {
981 __n_assert(repo, return -1);
982 __n_assert(repo->repo, return -1);
983 __n_assert(branch_name, return -1);
984
985 git_reference* branch_ref = NULL;
986 int ret = -1;
987
988 int err = git_branch_lookup(&branch_ref, repo->repo, branch_name, GIT_BRANCH_LOCAL);
989 if (err < 0) {
990 _n_git_log_error("n_git_switch_branch");
991 _n_git_set_status(repo, "switch to '%s' failed: branch not found", branch_name);
992 return -1;
993 }
994
995 char refname[512];
996 snprintf(refname, sizeof(refname), "refs/heads/%s", branch_name);
997
998 err = git_repository_set_head(repo->repo, refname);
999 if (err < 0) {
1000 _n_git_log_error("n_git_switch_branch");
1001 _n_git_set_status(repo, "switch to '%s' failed", branch_name);
1002 goto cleanup;
1003 }
1004
1005 git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT;
1006 opts.checkout_strategy = GIT_CHECKOUT_FORCE;
1007
1008 err = git_checkout_head(repo->repo, &opts);
1009 if (err < 0) {
1010 _n_git_log_error("n_git_switch_branch");
1011 _n_git_set_status(repo, "switch to '%s' failed", branch_name);
1012 goto cleanup;
1013 }
1014
1015 _n_git_set_status(repo, "switched to branch '%s'", branch_name);
1016 ret = 0;
1017
1018cleanup:
1019 git_reference_free(branch_ref);
1020 return ret;
1021} /* n_git_switch_branch */
1022
1030int n_git_current_branch(N_GIT_REPO* repo, char* out, size_t out_size) {
1031 __n_assert(repo, return -1);
1032 __n_assert(repo->repo, return -1);
1033 __n_assert(out, return -1);
1034 __n_assert(out_size > 0, return -1);
1035
1036 git_reference* head_ref = NULL;
1037
1038 int err = git_repository_head(&head_ref, repo->repo);
1039 if (err == 0) {
1040 const char* shorthand = git_reference_shorthand(head_ref);
1041 snprintf(out, out_size, "%s", shorthand ? shorthand : "");
1042 git_reference_free(head_ref);
1043 return 0;
1044 }
1045
1046 /* Handle unborn HEAD (empty repo, no commits yet).
1047 * Read the symbolic HEAD reference to determine the branch name. */
1048 if (err == GIT_EUNBORNBRANCH) {
1049 git_reference* sym_ref = NULL;
1050 int sym_err = git_reference_lookup(&sym_ref, repo->repo, "HEAD");
1051 if (sym_err == 0 && git_reference_type(sym_ref) == GIT_REFERENCE_SYMBOLIC) {
1052 const char* target = git_reference_symbolic_target(sym_ref);
1053 /* target is e.g. "refs/heads/main", extract branch name */
1054 if (target) {
1055 const char* prefix = "refs/heads/";
1056 size_t prefix_len = strlen(prefix);
1057 if (strncmp(target, prefix, prefix_len) == 0) {
1058 snprintf(out, out_size, "%s", target + prefix_len);
1059 } else {
1060 snprintf(out, out_size, "%s", target);
1061 }
1062 git_reference_free(sym_ref);
1063 return 0;
1064 }
1065 }
1066 if (sym_ref) git_reference_free(sym_ref);
1067 }
1068
1069 _n_git_log_error("n_git_current_branch");
1070 return -1;
1071} /* n_git_current_branch */
1072
1079int n_git_delete_branch(N_GIT_REPO* repo, const char* branch_name) {
1080 __n_assert(repo, return -1);
1081 __n_assert(repo->repo, return -1);
1082 __n_assert(branch_name, return -1);
1083
1084 git_reference* branch_ref = NULL;
1085
1086 int err = git_branch_lookup(&branch_ref, repo->repo, branch_name, GIT_BRANCH_LOCAL);
1087 if (err < 0) {
1088 _n_git_log_error("n_git_delete_branch");
1089 return -1;
1090 }
1091
1092 err = git_branch_delete(branch_ref);
1093 git_reference_free(branch_ref);
1094
1095 if (err < 0) {
1096 _n_git_log_error("n_git_delete_branch");
1097 return -1;
1098 }
1099
1100 return 0;
1101} /* n_git_delete_branch */
1102
1103/* REMOTE OPERATIONS: auth, push, pull */
1104
1108static int _n_git_cred_cb(git_credential** out, const char* url, const char* username_from_url, unsigned int allowed_types, void* payload) {
1109 (void)url;
1110 N_GIT_REMOTE_AUTH* auth = (N_GIT_REMOTE_AUTH*)payload;
1111 if (!auth) return GIT_PASSTHROUGH;
1112
1113 switch (auth->type) {
1114 case N_GIT_AUTH_TOKEN:
1115 if ((allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) &&
1116 auth->password) {
1117 const char* user = auth->username ? auth->username : "x-access-token";
1118 return git_credential_userpass_plaintext_new(out, user, auth->password);
1119 }
1120 break;
1121
1122 case N_GIT_AUTH_BASIC:
1123 if ((allowed_types & GIT_CREDENTIAL_USERPASS_PLAINTEXT) &&
1124 auth->username && auth->password) {
1125 return git_credential_userpass_plaintext_new(out, auth->username, auth->password);
1126 }
1127 break;
1128
1129 case N_GIT_AUTH_SSH:
1130 if ((allowed_types & GIT_CREDENTIAL_SSH_KEY) &&
1131 auth->ssh_privkey_path) {
1132 const char* user = auth->username ? auth->username : (username_from_url ? username_from_url : "git");
1133 return git_credential_ssh_key_new(out, user,
1134 auth->ssh_pubkey_path, auth->ssh_privkey_path,
1135 auth->ssh_passphrase ? auth->ssh_passphrase : "");
1136 }
1137 /* Try SSH agent if key auth not available */
1138 if (allowed_types & GIT_CREDENTIAL_SSH_KEY) {
1139 const char* user = auth->username ? auth->username : (username_from_url ? username_from_url : "git");
1140 return git_credential_ssh_key_from_agent(out, user);
1141 }
1142 break;
1143
1144 default:
1145 break;
1146 }
1147
1148 return GIT_PASSTHROUGH;
1149} /* _n_git_cred_cb */
1150
1155 __n_assert(repo, return -1);
1156 __n_assert(auth, return -1);
1157
1158 /* Free existing auth */
1159 if (repo->remote_auth) {
1165 FreeNoLog(repo->remote_auth);
1166 }
1167
1168 N_GIT_REMOTE_AUTH* a = NULL;
1170 __n_assert(a, return -1);
1171 memset(a, 0, sizeof(*a));
1172
1173 a->type = auth->type;
1174 if (auth->username) a->username = strdup(auth->username);
1175 if (auth->password) a->password = strdup(auth->password);
1176 if (auth->ssh_pubkey_path) a->ssh_pubkey_path = strdup(auth->ssh_pubkey_path);
1177 if (auth->ssh_privkey_path) a->ssh_privkey_path = strdup(auth->ssh_privkey_path);
1178 if (auth->ssh_passphrase) a->ssh_passphrase = strdup(auth->ssh_passphrase);
1179
1180 repo->remote_auth = a;
1181 return 0;
1182} /* n_git_set_remote_auth */
1183
1187int n_git_remote_set_url(N_GIT_REPO* repo, const char* url) {
1188 __n_assert(repo, return -1);
1189 __n_assert(repo->repo, return -1);
1190 __n_assert(url, return -1);
1191
1193
1194 git_remote* remote = NULL;
1195 int err = git_remote_lookup(&remote, repo->repo, "origin");
1196 if (err == 0) {
1197 /* Remote exists, update URL if different */
1198 const char* current_url = git_remote_url(remote);
1199 if (!current_url || strcmp(current_url, url) != 0) {
1200 git_remote_free(remote);
1201 err = git_remote_set_url(repo->repo, "origin", url);
1202 if (err < 0) {
1203 _n_git_log_error("n_git_remote_set_url: set_url");
1204 return -1;
1205 }
1206 } else {
1207 git_remote_free(remote);
1208 }
1209 } else {
1210 /* Remote does not exist, create it */
1211 err = git_remote_create(&remote, repo->repo, "origin", url);
1212 if (err < 0) {
1213 _n_git_log_error("n_git_remote_set_url: create");
1214 return -1;
1215 }
1216 git_remote_free(remote);
1217 }
1218
1219 return 0;
1220} /* n_git_remote_set_url */
1221
1226 __n_assert(repo, return -1);
1227 __n_assert(repo->repo, return -1);
1228
1230
1231 /* Refuse to push an empty repo (no commits yet).
1232 * git_repository_head_unborn() returns 1 if unborn, 0 if not, <0 on error. */
1233 int unborn = git_repository_head_unborn(repo->repo);
1234 if (unborn < 0) {
1235 _n_git_log_error("n_git_push: head_unborn");
1236 _n_git_set_status(repo, "push failed: unable to check HEAD");
1237 return -1;
1238 }
1239 if (unborn == 1) {
1240 n_log(LOG_ERR, "n_git_push: repository has no commits, commit first before pushing");
1241 _n_git_set_status(repo, "push failed: no commits yet, commit first");
1242 return -1;
1243 }
1244
1245 /* Get current branch name */
1246 char branch[128];
1247 if (n_git_current_branch(repo, branch, sizeof(branch)) != 0) {
1248 n_log(LOG_ERR, "n_git_push: cannot determine current branch");
1249 _n_git_set_status(repo, "push failed: cannot determine current branch");
1250 return -1;
1251 }
1252
1253 /* Build refspec: refs/heads/branch:refs/heads/branch */
1254 char refspec[300];
1255 snprintf(refspec, sizeof(refspec), "refs/heads/%s:refs/heads/%s",
1256 branch, branch);
1257
1258 git_remote* remote = NULL;
1259 int err = git_remote_lookup(&remote, repo->repo, "origin");
1260 if (err < 0) {
1261 _n_git_log_error("n_git_push: remote lookup");
1262 _n_git_set_status(repo, "push failed: remote 'origin' not found");
1263 return -1;
1264 }
1265
1266 /* Set up push options with credential callback */
1267 git_push_options opts = GIT_PUSH_OPTIONS_INIT;
1268 if (repo->remote_auth) {
1269 opts.callbacks.credentials = _n_git_cred_cb;
1270 opts.callbacks.payload = repo->remote_auth;
1271 }
1272
1273 const char* refspecs[] = {refspec};
1274 git_strarray refspec_arr = {(char**)refspecs, 1};
1275
1276 err = git_remote_push(remote, &refspec_arr, &opts);
1277 git_remote_free(remote);
1278
1279 if (err < 0) {
1280 const git_error* gerr = git_error_last();
1281 _n_git_log_error("n_git_push");
1282 _n_git_set_status(repo, "push failed: %s",
1283 (gerr && gerr->message) ? gerr->message : "unknown error");
1284 return -1;
1285 }
1286
1287 n_log(LOG_INFO, "n_git_push: pushed %s to origin", branch);
1288 _n_git_set_status(repo, "pushed '%s' to origin", branch);
1289 return 0;
1290} /* n_git_push */
1291
1296 __n_assert(repo, return -1);
1297 __n_assert(repo->repo, return -1);
1298
1300
1301 /* Fetch from origin */
1302 git_remote* remote = NULL;
1303 int err = git_remote_lookup(&remote, repo->repo, "origin");
1304 if (err < 0) {
1305 _n_git_log_error("n_git_pull: remote lookup");
1306 _n_git_set_status(repo, "pull failed: remote 'origin' not found");
1307 return -1;
1308 }
1309
1310 git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
1311 if (repo->remote_auth) {
1312 fetch_opts.callbacks.credentials = _n_git_cred_cb;
1313 fetch_opts.callbacks.payload = repo->remote_auth;
1314 }
1315
1316 err = git_remote_fetch(remote, NULL, &fetch_opts, "pull");
1317 git_remote_free(remote);
1318
1319 if (err < 0) {
1320 const git_error* gerr = git_error_last();
1321 _n_git_log_error("n_git_pull: fetch");
1322 _n_git_set_status(repo, "pull failed: fetch error: %s",
1323 (gerr && gerr->message) ? gerr->message : "unknown error");
1324 return -1;
1325 }
1326
1327 /* Get current branch name */
1328 char branch[128];
1329 if (n_git_current_branch(repo, branch, sizeof(branch)) != 0) {
1330 n_log(LOG_ERR, "n_git_pull: cannot determine current branch");
1331 _n_git_set_status(repo, "pull failed: cannot determine current branch");
1332 return -1;
1333 }
1334
1335 /* Look up the remote tracking ref: refs/remotes/origin/<branch> */
1336 char remote_ref[300];
1337 snprintf(remote_ref, sizeof(remote_ref), "refs/remotes/origin/%s", branch);
1338
1339 git_oid remote_oid;
1340 err = git_reference_name_to_id(&remote_oid, repo->repo, remote_ref);
1341 if (err < 0) {
1342 _n_git_log_error("n_git_pull: remote ref lookup");
1343 _n_git_set_status(repo, "pull failed: no remote tracking branch for '%s'", branch);
1344 return -1;
1345 }
1346
1347 /* Get HEAD commit */
1348 git_oid local_oid;
1349 err = git_reference_name_to_id(&local_oid, repo->repo, "HEAD");
1350 if (err < 0) {
1351 _n_git_log_error("n_git_pull: HEAD lookup");
1352 _n_git_set_status(repo, "pull failed: cannot read HEAD");
1353 return -1;
1354 }
1355
1356 /* Check if already up to date */
1357 if (git_oid_equal(&local_oid, &remote_oid)) {
1358 n_log(LOG_INFO, "n_git_pull: already up to date");
1359 _n_git_set_status(repo, "already up to date");
1360 return 0;
1361 }
1362
1363 /* Check if fast-forward is possible */
1364 git_annotated_commit* remote_commit = NULL;
1365 err = git_annotated_commit_lookup(&remote_commit, repo->repo, &remote_oid);
1366 if (err < 0) {
1367 _n_git_log_error("n_git_pull: annotated commit lookup");
1368 _n_git_set_status(repo, "pull failed: cannot lookup remote commit");
1369 return -1;
1370 }
1371
1372 git_merge_analysis_t analysis;
1373 git_merge_preference_t preference;
1374 const git_annotated_commit* their_heads[] = {remote_commit};
1375 err = git_merge_analysis(&analysis, &preference, repo->repo, their_heads, 1);
1376 if (err < 0) {
1377 git_annotated_commit_free(remote_commit);
1378 _n_git_log_error("n_git_pull: merge analysis");
1379 _n_git_set_status(repo, "pull failed: merge analysis error");
1380 return -1;
1381 }
1382
1383 if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
1384 git_annotated_commit_free(remote_commit);
1385 n_log(LOG_INFO, "n_git_pull: already up to date");
1386 _n_git_set_status(repo, "already up to date");
1387 return 0;
1388 }
1389
1390 if (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD) {
1391 /* Fast-forward: move HEAD to remote commit */
1392 git_reference* head_ref = NULL;
1393 err = git_repository_head(&head_ref, repo->repo);
1394 if (err < 0) {
1395 git_annotated_commit_free(remote_commit);
1396 _n_git_log_error("n_git_pull: get HEAD ref");
1397 _n_git_set_status(repo, "pull failed: cannot get HEAD reference");
1398 return -1;
1399 }
1400
1401 git_reference* new_ref = NULL;
1402 err = git_reference_set_target(&new_ref, head_ref, &remote_oid, "pull: fast-forward");
1403 git_reference_free(head_ref);
1404 if (err < 0) {
1405 git_annotated_commit_free(remote_commit);
1406 _n_git_log_error("n_git_pull: set target");
1407 _n_git_set_status(repo, "pull failed: cannot fast-forward HEAD");
1408 return -1;
1409 }
1410 git_reference_free(new_ref);
1411
1412 /* Checkout the new HEAD */
1413 git_object* target = NULL;
1414 git_object_lookup(&target, repo->repo, &remote_oid, GIT_OBJECT_COMMIT);
1415 if (target) {
1416 git_checkout_options co_opts = GIT_CHECKOUT_OPTIONS_INIT;
1417 co_opts.checkout_strategy = GIT_CHECKOUT_SAFE;
1418 git_checkout_tree(repo->repo, target, &co_opts);
1419 git_object_free(target);
1420 }
1421
1422 git_annotated_commit_free(remote_commit);
1423 n_log(LOG_INFO, "n_git_pull: fast-forwarded %s", branch);
1424 _n_git_set_status(repo, "pulled '%s': fast-forwarded to latest", branch);
1425 return 0;
1426 }
1427
1428 /* Not a fast-forward, we don't handle merge conflicts */
1429 git_annotated_commit_free(remote_commit);
1430 n_log(LOG_ERR, "n_git_pull: cannot fast-forward, manual merge required");
1431 _n_git_set_status(repo, "pull failed: cannot fast-forward, manual merge required");
1432 return -1;
1433} /* n_git_pull */
1434
1439 __n_assert(repo, return -1);
1440 __n_assert(repo->repo, return -1);
1441
1443
1444 git_remote* remote = NULL;
1445 int err = git_remote_lookup(&remote, repo->repo, "origin");
1446 if (err < 0) {
1447 _n_git_log_error("n_git_fetch: remote lookup");
1448 _n_git_set_status(repo, "fetch failed: remote 'origin' not found");
1449 return -1;
1450 }
1451
1452 git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
1453 if (repo->remote_auth) {
1454 fetch_opts.callbacks.credentials = _n_git_cred_cb;
1455 fetch_opts.callbacks.payload = repo->remote_auth;
1456 }
1457
1458 err = git_remote_fetch(remote, NULL, &fetch_opts, "fetch");
1459 git_remote_free(remote);
1460
1461 if (err < 0) {
1462 const git_error* gerr = git_error_last();
1463 _n_git_log_error("n_git_fetch");
1464 _n_git_set_status(repo, "fetch failed: %s",
1465 (gerr && gerr->message) ? gerr->message : "unknown error");
1466 return -1;
1467 }
1468
1469 n_log(LOG_INFO, "n_git_fetch: fetched from origin");
1470 _n_git_set_status(repo, "fetched from origin");
1471 return 0;
1472} /* n_git_fetch */
1473
1477int n_git_ahead_behind(N_GIT_REPO* repo, size_t* ahead, size_t* behind) {
1478 __n_assert(repo, return -1);
1479 __n_assert(repo->repo, return -1);
1480
1482
1483 if (ahead) *ahead = 0;
1484 if (behind) *behind = 0;
1485
1486 /* Check for unborn HEAD (no commits yet) */
1487 int unborn = git_repository_head_unborn(repo->repo);
1488 if (unborn < 0) {
1489 _n_git_log_error("n_git_ahead_behind: head_unborn");
1490 _n_git_set_status(repo, "ahead/behind: unable to check HEAD");
1491 return -1;
1492 }
1493 if (unborn == 1) {
1494 /* No commits yet, nothing to compare */
1495 _n_git_set_status(repo, "no commits yet");
1496 return 0;
1497 }
1498
1499 /* Get current branch name, 256 bytes covers practical branch names */
1500 char branch[256];
1501 if (n_git_current_branch(repo, branch, sizeof(branch)) != 0) {
1502 _n_git_set_status(repo, "ahead/behind: cannot determine current branch");
1503 return -1;
1504 }
1505
1506 /* Resolve local HEAD */
1507 git_oid local_oid;
1508 int err = git_reference_name_to_id(&local_oid, repo->repo, "HEAD");
1509 if (err < 0) {
1510 _n_git_log_error("n_git_ahead_behind: HEAD lookup");
1511 _n_git_set_status(repo, "ahead/behind: cannot read HEAD");
1512 return -1;
1513 }
1514
1515 /* Resolve remote tracking ref */
1516 char remote_ref[512];
1517 int ref_len = snprintf(remote_ref, sizeof(remote_ref),
1518 "refs/remotes/origin/%s", branch);
1519 if (ref_len < 0 || (size_t)ref_len >= sizeof(remote_ref)) {
1520 _n_git_set_status(repo, "ahead/behind: branch name too long");
1521 return -1;
1522 }
1523 git_oid remote_oid;
1524 err = git_reference_name_to_id(&remote_oid, repo->repo, remote_ref);
1525 if (err < 0) {
1526 /* Distinguish "no tracking branch" from other libgit2 errors */
1527 if (err == GIT_ENOTFOUND || err == GIT_EINVALIDSPEC) {
1528 _n_git_set_status(repo, "ahead/behind: no remote tracking branch for '%s'", branch);
1529 } else {
1530 const git_error* e;
1531 _n_git_log_error("n_git_ahead_behind: remote tracking ref lookup");
1532 e = git_error_last();
1533 if (e && e->message) {
1534 _n_git_set_status(repo, "ahead/behind: %s", e->message);
1535 } else {
1536 _n_git_set_status(repo, "ahead/behind: failed to read remote tracking branch");
1537 }
1538 }
1539 return -1;
1540 }
1541
1542 /* Already equal? */
1543 if (git_oid_equal(&local_oid, &remote_oid)) {
1544 _n_git_set_status(repo, "up to date with origin/%s", branch);
1545 return 0;
1546 }
1547
1548 /* Compute ahead/behind counts */
1549 size_t a = 0, b = 0;
1550 err = git_graph_ahead_behind(&a, &b, repo->repo, &local_oid, &remote_oid);
1551 if (err < 0) {
1552 _n_git_log_error("n_git_ahead_behind: graph_ahead_behind");
1553 _n_git_set_status(repo, "ahead/behind: comparison failed");
1554 return -1;
1555 }
1556
1557 if (ahead) *ahead = a;
1558 if (behind) *behind = b;
1559
1560 _n_git_set_status(repo, "origin/%s: %zu ahead, %zu behind", branch, a, b);
1561 return 0;
1562} /* n_git_ahead_behind */
1563
1564#pragma GCC diagnostic pop
1565
1566#endif /* HAVE_LIBGIT2 */
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:272
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
char short_hash[10]
short (abbreviated) hex hash
Definition n_git.h:105
char author[256]
author name and email
Definition n_git.h:109
int64_t timestamp
unix timestamp of the commit
Definition n_git.h:111
char message[512]
first line of commit message
Definition n_git.h:107
char * password
password or personal access token, or NULL
Definition n_git.h:71
char * ssh_privkey_path
path to SSH private key file, or NULL
Definition n_git.h:75
char hash[42]
full hex hash
Definition n_git.h:103
char * ssh_pubkey_path
path to SSH public key file, or NULL (derived from private key + ".pub")
Definition n_git.h:73
int flags
combination of N_GIT_STATUS_* flags
Definition n_git.h:97
char * path
filesystem path to the repository
Definition n_git.h:85
git_repository * repo
libgit2 repository handle
Definition n_git.h:83
char * ssh_passphrase
passphrase for SSH key, or NULL
Definition n_git.h:77
char last_status[512]
human-readable status message from the last operation
Definition n_git.h:89
int type
auth type: N_GIT_AUTH_NONE/TOKEN/BASIC/SSH
Definition n_git.h:67
char * username
username for basic auth or SSH, or NULL
Definition n_git.h:69
char path[512]
relative path of the file
Definition n_git.h:95
N_GIT_REMOTE_AUTH * remote_auth
credentials for remote operations, or NULL
Definition n_git.h:87
void n_git_close(N_GIT_REPO **repo)
Close a Git repository and free resources.
Definition n_git.c:241
#define N_GIT_AUTH_TOKEN
Remote auth: personal access token (used as password with empty or "x-access-token" user)
Definition n_git.h:58
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:714
#define N_GIT_STATUS_NEW
File is new / untracked.
Definition n_git.h:47
int n_git_fetch(N_GIT_REPO *repo)
Fetch from the "origin" remote without merging.
Definition n_git.c:1438
int n_git_push(N_GIT_REPO *repo)
Push the current branch to the "origin" remote.
Definition n_git.c:1225
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:1187
int n_git_switch_branch(N_GIT_REPO *repo, const char *branch_name)
Switch to an existing local branch.
Definition n_git.c:980
int n_git_pull(N_GIT_REPO *repo)
Fetch from "origin" and fast-forward merge the current branch.
Definition n_git.c:1295
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:796
#define N_GIT_STATUS_MODIFIED
File has been modified.
Definition n_git.h:49
LIST * n_git_list_branches(N_GIT_REPO *repo)
List all local branch names.
Definition n_git.c:868
LIST * n_git_status(N_GIT_REPO *repo)
Get the status of all files in the working directory.
Definition n_git.c:266
int n_git_unstage(N_GIT_REPO *repo, const char *filepath)
Unstage a single file (reset from HEAD).
Definition n_git.c:458
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:1154
LIST * n_git_log(N_GIT_REPO *repo, size_t max_entries)
Retrieve commit log entries starting from HEAD.
Definition n_git.c:596
#define N_GIT_STATUS_STAGED
File is staged in the index.
Definition n_git.h:53
#define N_GIT_AUTH_SSH
Remote auth: SSH key file.
Definition n_git.h:62
N_GIT_REPO * n_git_init_ex(const char *path, const char *initial_branch)
Initialize a new Git repository with a custom initial branch name.
Definition n_git.c:205
#define N_GIT_AUTH_BASIC
Remote auth: username + password.
Definition n_git.h:60
int n_git_stage_all(N_GIT_REPO *repo)
Stage all modified and untracked files.
Definition n_git.c:416
int n_git_stage(N_GIT_REPO *repo, const char *filepath)
Stage a file or directory by path.
Definition n_git.c:354
int n_git_ahead_behind(N_GIT_REPO *repo, size_t *ahead, size_t *behind)
Count commits ahead and behind relative to the remote tracking branch.
Definition n_git.c:1477
int n_git_create_branch(N_GIT_REPO *repo, const char *branch_name)
Create a new branch from HEAD.
Definition n_git.c:911
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:1030
int n_git_checkout_commit(N_GIT_REPO *repo, const char *hash)
Checkout a specific commit (detached HEAD).
Definition n_git.c:827
#define N_GIT_STATUS_DELETED
File has been deleted.
Definition n_git.h:51
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:507
N_GIT_REPO * n_git_open(const char *path)
Open an existing Git repository.
Definition n_git.c:163
N_STR * n_git_diff_workdir(N_GIT_REPO *repo)
Get a diff of the working directory against the index.
Definition n_git.c:684
N_GIT_REPO * n_git_init(const char *path)
Initialize a new Git repository.
Definition n_git.c:195
int n_git_delete_branch(N_GIT_REPO *repo, const char *branch_name)
Delete a local branch.
Definition n_git.c:1079
Commit metadata.
Definition n_git.h:101
Credentials for remote operations.
Definition n_git.h:65
Wrapper around a git_repository handle.
Definition n_git.h:81
Single file status entry.
Definition n_git.h:93
#define UNLIMITED_LIST_ITEMS
flag to pass to new_generic_list for an unlimited number of item in the list.
Definition n_list.h:73
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:228
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:37
Structure of a generic LIST container.
Definition n_list.h:59
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:89
#define LOG_ERR
error conditions
Definition n_log.h:76
#define LOG_INFO
informational
Definition n_log.h:82
#define nstrprintf_cat(__nstr_var, __format,...)
Macro to quickly allocate and sprintf and cat to a N_STR.
Definition n_str.h:121
N_STR * new_nstr(NSTRBYTE size)
create a new N_STR string
Definition n_str.c:207
A box including a string and his lenght.
Definition n_str.h:61
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:1108
static void _n_git_status_entry_destroy(void *ptr)
Destructor for N_GIT_STATUS_ENTRY used by LIST.
Definition n_git.c:110
static void _n_git_string_destroy(void *ptr)
Destructor for char* branch name strings used by LIST.
Definition n_git.c:128
static void _n_git_log_error(const char *func_name)
Log the last libgit2 error.
Definition n_git.c:74
static void _n_git_ensure_init(void)
Ensure libgit2 is initialized (idempotent).
Definition n_git.c:56
#define GIT_OID_SHA1_HEXSIZE
Definition n_git.c:47
static void _n_git_commit_info_destroy(void *ptr)
Destructor for N_GIT_COMMIT_INFO used by LIST.
Definition n_git.c:119
static void _n_git_set_status(N_GIT_REPO *repo, const char *fmt,...)
Set the last_status message on a repo handle.
Definition n_git.c:93
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:141
static int _n_git_initialized
static flag ensuring git_libgit2_init is called only once
Definition n_git.c:51
libgit2 wrapper for Git repository operations
Generic log system.
__attribute__((unused))
Definition n_network.c:1286