C · 21936 bytes Raw Blame History
1 /* Git configuration operations with comprehensive validation and security
2 * Implements safe git configuration management for gitswitch-c
3 */
4
5 #define _POSIX_C_SOURCE 200809L
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <unistd.h>
10 #include <sys/wait.h>
11 #include <sys/stat.h>
12
13 #include "git_ops.h"
14 #include "error.h"
15 #include "utils.h"
16 #include "display.h"
17
18 /* Internal helper functions */
19 static int execute_git_command(const char *args, char *output, size_t output_size);
20 static int validate_git_installation(void);
21 static bool is_valid_git_config_value(const char *value);
22 static int backup_git_config_if_needed(git_scope_t scope);
23
24 /* Initialize git operations */
25 int git_ops_init(void) {
26 log_debug("Initializing git operations");
27
28 /* Validate git installation */
29 if (validate_git_installation() != 0) {
30 set_error(ERR_SYSTEM_REQUIREMENT, "Git validation failed");
31 return -1;
32 }
33
34 log_info("Git operations initialized successfully");
35 return 0;
36 }
37
38 /* Set git configuration for account */
39 int git_set_config(const account_t *account, git_scope_t scope) {
40 const char *scope_flag;
41
42 if (!account) {
43 set_error(ERR_INVALID_ARGS, "NULL account to git_set_config");
44 return -1;
45 }
46
47 /* Validate account data */
48 if (!validate_name(account->name)) {
49 set_error(ERR_ACCOUNT_INVALID, "Invalid account name for git config");
50 return -1;
51 }
52
53 if (!validate_email(account->email)) {
54 set_error(ERR_ACCOUNT_INVALID, "Invalid account email for git config");
55 return -1;
56 }
57
58 /* Get scope flag */
59 scope_flag = git_scope_to_flag(scope);
60 if (!scope_flag) {
61 set_error(ERR_INVALID_ARGS, "Invalid git scope");
62 return -1;
63 }
64
65 /* If local scope, ensure we're in a git repository */
66 if (scope == GIT_SCOPE_LOCAL && !git_is_repository()) {
67 set_error(ERR_GIT_NOT_REPOSITORY, "Not in a git repository, cannot set local config");
68 return -1;
69 }
70
71 /* Backup current configuration if requested */
72 if (backup_git_config_if_needed(scope) != 0) {
73 log_warning("Failed to backup git configuration");
74 }
75
76 /* When setting global scope inside a repo, clear local config so stale
77 * values (e.g. signing key from a prior account) don't take precedence */
78 if (scope == GIT_SCOPE_GLOBAL && git_is_repository()) {
79 log_info("Clearing local git config to prevent stale overrides");
80 git_clear_config(GIT_SCOPE_LOCAL);
81 }
82
83 log_info("Setting git configuration for account: %s (%s scope)", account->name, scope_flag);
84
85 /* Set user.name */
86 if (git_set_config_value(GIT_CONFIG_USER_NAME, account->name, scope) != 0) {
87 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set user.name");
88 return -1;
89 }
90
91 /* Set user.email */
92 if (git_set_config_value(GIT_CONFIG_USER_EMAIL, account->email, scope) != 0) {
93 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set user.email");
94 return -1;
95 }
96
97 /* Configure GPG if enabled */
98 if (account->gpg_enabled) {
99 if (git_configure_gpg(account, scope) != 0) {
100 log_warning("Failed to configure GPG for git");
101 /* Don't fail completely, GPG is optional */
102 }
103 } else {
104 /* Disable GPG signing */
105 git_unset_config_value(GIT_CONFIG_USER_SIGNINGKEY, scope);
106 git_set_config_value(GIT_CONFIG_COMMIT_GPGSIGN, "false", scope);
107 }
108
109 /* Configure SSH if enabled */
110 if (account->ssh_enabled && strlen(account->ssh_key_path) > 0) {
111 if (git_configure_ssh(account, scope) != 0) {
112 log_warning("Failed to configure SSH for git");
113 /* Don't fail completely, SSH config is optional */
114 }
115 } else {
116 /* Clear SSH configuration */
117 git_unset_config_value(GIT_CONFIG_CORE_SSHCOMMAND, scope);
118 }
119
120 /* Verify configuration was set correctly - check the same scope we just wrote to */
121 char verify_name[MAX_NAME_LEN];
122 char verify_email[MAX_EMAIL_LEN];
123 if (git_get_config_value(GIT_CONFIG_USER_NAME, verify_name, sizeof(verify_name), scope) == 0 &&
124 git_get_config_value(GIT_CONFIG_USER_EMAIL, verify_email, sizeof(verify_email), scope) == 0) {
125 if (strcmp(verify_name, account->name) != 0 ||
126 strcmp(verify_email, account->email) != 0) {
127 set_error(ERR_GIT_CONFIG_FAILED, "Git configuration verification failed");
128 return -1;
129 }
130 }
131
132 log_info("Git configuration set successfully for %s", account->name);
133 return 0;
134 }
135
136 /* Get current git configuration */
137 int git_get_current_config(git_current_config_t *config) {
138 char name[MAX_NAME_LEN];
139 char email[MAX_EMAIL_LEN];
140 char signing_key[MAX_KEY_ID_LEN];
141 char gpg_sign[16];
142
143 if (!config) {
144 set_error(ERR_INVALID_ARGS, "NULL config to git_get_current_config");
145 return -1;
146 }
147
148 /* Initialize structure */
149 memset(config, 0, sizeof(git_current_config_t));
150 config->valid = false;
151
152 /* Try to get user.name */
153 if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_LOCAL) == 0) {
154 config->scope = GIT_SCOPE_LOCAL;
155 } else if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_GLOBAL) == 0) {
156 config->scope = GIT_SCOPE_GLOBAL;
157 } else if (git_get_config_value(GIT_CONFIG_USER_NAME, name, sizeof(name), GIT_SCOPE_SYSTEM) == 0) {
158 config->scope = GIT_SCOPE_SYSTEM;
159 } else {
160 set_error(ERR_GIT_CONFIG_NOT_FOUND, "No git user.name configured");
161 return -1;
162 }
163
164 /* Get user.email from same scope */
165 if (git_get_config_value(GIT_CONFIG_USER_EMAIL, email, sizeof(email), config->scope) != 0) {
166 set_error(ERR_GIT_CONFIG_NOT_FOUND, "No git user.email configured");
167 return -1;
168 }
169
170 /* Copy basic configuration */
171 safe_strncpy(config->name, name, sizeof(config->name));
172 safe_strncpy(config->email, email, sizeof(config->email));
173
174 /* Get GPG configuration if available */
175 if (git_get_config_value(GIT_CONFIG_USER_SIGNINGKEY, signing_key, sizeof(signing_key), config->scope) == 0) {
176 safe_strncpy(config->signing_key, signing_key, sizeof(config->signing_key));
177 }
178
179 /* Check if GPG signing is enabled */
180 if (git_get_config_value(GIT_CONFIG_COMMIT_GPGSIGN, gpg_sign, sizeof(gpg_sign), config->scope) == 0) {
181 config->gpg_signing_enabled = (strcmp(gpg_sign, "true") == 0);
182 }
183
184 config->valid = true;
185 return 0;
186 }
187
188 /* Clear git configuration */
189 int git_clear_config(git_scope_t scope) {
190 const char *scope_flag;
191
192 scope_flag = git_scope_to_flag(scope);
193 if (!scope_flag) {
194 set_error(ERR_INVALID_ARGS, "Invalid git scope");
195 return -1;
196 }
197
198 log_info("Clearing git configuration (%s scope)", scope_flag);
199
200 /* Clear basic user configuration */
201 git_unset_config_value(GIT_CONFIG_USER_NAME, scope);
202 git_unset_config_value(GIT_CONFIG_USER_EMAIL, scope);
203
204 /* Clear GPG configuration */
205 git_unset_config_value(GIT_CONFIG_USER_SIGNINGKEY, scope);
206 git_unset_config_value(GIT_CONFIG_COMMIT_GPGSIGN, scope);
207 git_unset_config_value(GIT_CONFIG_GPG_PROGRAM, scope);
208
209 /* Clear SSH configuration */
210 git_unset_config_value(GIT_CONFIG_CORE_SSHCOMMAND, scope);
211
212 log_info("Git configuration cleared");
213 return 0;
214 }
215
216 /* Validate git repository */
217 int git_validate_repository(void) {
218 char output[256];
219
220 if (!git_is_repository()) {
221 set_error(ERR_GIT_NOT_REPOSITORY, "Current directory is not a git repository");
222 return -1;
223 }
224
225 /* Check if repository is bare */
226 if (execute_git_command("rev-parse --is-bare-repository", output, sizeof(output)) == 0) {
227 trim_whitespace(output);
228 if (strcmp(output, "true") == 0) {
229 set_error(ERR_GIT_REPOSITORY_INVALID, "Repository is bare");
230 return -1;
231 }
232 }
233
234 /* Check repository health - verify we can read HEAD */
235 if (execute_git_command("rev-parse --verify HEAD", output, sizeof(output)) != 0) {
236 /* This is OK for new repositories with no commits */
237 log_debug("Repository has no commits yet (new repository)");
238 }
239
240 return 0;
241 }
242
243 /* Get git configuration scope */
244 git_scope_t git_get_config_scope(const char *config_key) {
245 char value[512];
246
247 if (!config_key) {
248 return GIT_SCOPE_GLOBAL; /* Default fallback */
249 }
250
251 /* Try local scope first if we're in a repository */
252 if (git_is_repository()) {
253 if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_LOCAL) == 0) {
254 return GIT_SCOPE_LOCAL;
255 }
256 }
257
258 /* Try global scope */
259 if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_GLOBAL) == 0) {
260 return GIT_SCOPE_GLOBAL;
261 }
262
263 /* Try system scope */
264 if (git_get_config_value(config_key, value, sizeof(value), GIT_SCOPE_SYSTEM) == 0) {
265 return GIT_SCOPE_SYSTEM;
266 }
267
268 /* Default to global if not found */
269 return GIT_SCOPE_GLOBAL;
270 }
271
272 /* Test git configuration */
273 int git_test_config(const account_t *account, git_scope_t scope) {
274 char verify_name[MAX_NAME_LEN];
275 char verify_email[MAX_EMAIL_LEN];
276
277 if (!account) {
278 set_error(ERR_INVALID_ARGS, "NULL account to git_test_config");
279 return -1;
280 }
281
282 log_info("Testing git configuration for account: %s", account->name);
283
284 /* Get configuration from the specified scope and verify it matches */
285 if (git_get_config_value(GIT_CONFIG_USER_NAME, verify_name, sizeof(verify_name), scope) != 0 ||
286 git_get_config_value(GIT_CONFIG_USER_EMAIL, verify_email, sizeof(verify_email), scope) != 0) {
287 set_error(ERR_GIT_CONFIG_FAILED, "Failed to read git configuration from %s scope",
288 git_scope_to_flag(scope));
289 return -1;
290 }
291
292 if (strcmp(verify_name, account->name) != 0) {
293 set_error(ERR_GIT_CONFIG_FAILED, "Git user.name does not match account: expected '%s', got '%s'",
294 account->name, verify_name);
295 return -1;
296 }
297
298 if (strcmp(verify_email, account->email) != 0) {
299 set_error(ERR_GIT_CONFIG_FAILED, "Git user.email does not match account: expected '%s', got '%s'",
300 account->email, verify_email);
301 return -1;
302 }
303
304 /* Test GPG configuration if enabled */
305 if (account->gpg_enabled && strlen(account->gpg_key_id) > 0) {
306 char signing_key[MAX_KEY_ID_LEN];
307 char gpg_sign[16];
308
309 if (git_get_config_value(GIT_CONFIG_USER_SIGNINGKEY, signing_key, sizeof(signing_key), scope) != 0 ||
310 strlen(signing_key) == 0) {
311 set_error(ERR_GIT_CONFIG_FAILED, "GPG signing key not configured in git");
312 return -1;
313 }
314
315 if (git_get_config_value(GIT_CONFIG_COMMIT_GPGSIGN, gpg_sign, sizeof(gpg_sign), scope) != 0 ||
316 strcmp(gpg_sign, "true") != 0) {
317 log_warning("GPG signing is configured but not enabled");
318 }
319
320 /* Test GPG key availability */
321 char gpg_test[256];
322 snprintf(gpg_test, sizeof(gpg_test), "gpg --list-secret-keys '%s' >/dev/null 2>&1",
323 account->gpg_key_id);
324 if (system(gpg_test) != 0) {
325 set_error(ERR_GPG_KEY_NOT_FOUND, "GPG key not available: %s", account->gpg_key_id);
326 return -1;
327 }
328 }
329
330 log_info("Git configuration test passed for %s", account->name);
331 return 0;
332 }
333
334 /* Set single git configuration value */
335 int git_set_config_value(const char *key, const char *value, git_scope_t scope) {
336 char command[1024];
337 char output[256];
338 const char *scope_flag;
339
340 if (!key || !value) {
341 set_error(ERR_INVALID_ARGS, "NULL key or value to git_set_config_value");
342 return -1;
343 }
344
345 if (!is_valid_git_config_value(value)) {
346 set_error(ERR_INVALID_ARGS, "Invalid characters in git config value");
347 return -1;
348 }
349
350 scope_flag = git_scope_to_flag(scope);
351 if (!scope_flag) {
352 set_error(ERR_INVALID_ARGS, "Invalid git scope");
353 return -1;
354 }
355
356 /* Build git config command */
357 if ((size_t)snprintf(command, sizeof(command), "config %s '%s' '%s'",
358 scope_flag, key, value) >= sizeof(command)) {
359 set_error(ERR_INVALID_ARGS, "Git config command too long");
360 return -1;
361 }
362
363 log_debug("Setting git config: %s = %s (%s)", key, value, scope_flag);
364
365 if (execute_git_command(command, output, sizeof(output)) != 0) {
366 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set git config %s: %s", key, output);
367 return -1;
368 }
369
370 return 0;
371 }
372
373 /* Get single git configuration value */
374 int git_get_config_value(const char *key, char *value, size_t value_size, git_scope_t scope) {
375 char command[512];
376 char output[512];
377 const char *scope_flag;
378
379 if (!key || !value || value_size == 0) {
380 set_error(ERR_INVALID_ARGS, "Invalid arguments to git_get_config_value");
381 return -1;
382 }
383
384 scope_flag = git_scope_to_flag(scope);
385 if (!scope_flag) {
386 set_error(ERR_INVALID_ARGS, "Invalid git scope");
387 return -1;
388 }
389
390 /* Build git config command */
391 if ((size_t)snprintf(command, sizeof(command), "config %s '%s'", scope_flag, key) >= sizeof(command)) {
392 set_error(ERR_INVALID_ARGS, "Git config command too long");
393 return -1;
394 }
395
396 if (execute_git_command(command, output, sizeof(output)) != 0) {
397 /* Config value not found - this is not always an error */
398 value[0] = '\0';
399 return -1;
400 }
401
402 /* Remove trailing newline */
403 trim_whitespace(output);
404 safe_strncpy(value, output, value_size);
405
406 return 0;
407 }
408
409 /* Unset git configuration value */
410 int git_unset_config_value(const char *key, git_scope_t scope) {
411 char command[512];
412 char output[256];
413 const char *scope_flag;
414
415 if (!key) {
416 set_error(ERR_INVALID_ARGS, "NULL key to git_unset_config_value");
417 return -1;
418 }
419
420 scope_flag = git_scope_to_flag(scope);
421 if (!scope_flag) {
422 set_error(ERR_INVALID_ARGS, "Invalid git scope");
423 return -1;
424 }
425
426 /* Build git config unset command */
427 if ((size_t)snprintf(command, sizeof(command), "config %s --unset '%s'",
428 scope_flag, key) >= sizeof(command)) {
429 set_error(ERR_INVALID_ARGS, "Git config command too long");
430 return -1;
431 }
432
433 log_debug("Unsetting git config: %s (%s)", key, scope_flag);
434
435 /* Execute command - ignore errors as key might not exist */
436 execute_git_command(command, output, sizeof(output));
437
438 return 0;
439 }
440
441 /* List all git configuration values */
442 int git_list_config(git_scope_t scope, char *output, size_t output_size) {
443 char command[256];
444 const char *scope_flag;
445
446 if (!output || output_size == 0) {
447 set_error(ERR_INVALID_ARGS, "Invalid arguments to git_list_config");
448 return -1;
449 }
450
451 scope_flag = git_scope_to_flag(scope);
452 if (!scope_flag) {
453 set_error(ERR_INVALID_ARGS, "Invalid git scope");
454 return -1;
455 }
456
457 /* Build git config list command */
458 if ((size_t)snprintf(command, sizeof(command), "config %s --list", scope_flag) >= sizeof(command)) {
459 set_error(ERR_INVALID_ARGS, "Git config command too long");
460 return -1;
461 }
462
463 if (execute_git_command(command, output, output_size) != 0) {
464 set_error(ERR_GIT_CONFIG_FAILED, "Failed to list git configuration");
465 return -1;
466 }
467
468 return 0;
469 }
470
471 /* Configure SSH command for git operations */
472 int git_configure_ssh(const account_t *account, git_scope_t scope) {
473 char ssh_command[MAX_PATH_LEN * 2];
474 char expanded_key_path[MAX_PATH_LEN];
475
476 if (!account || !account->ssh_enabled || strlen(account->ssh_key_path) == 0) {
477 return 0; /* Nothing to configure */
478 }
479
480 /* Expand SSH key path */
481 if (expand_path(account->ssh_key_path, expanded_key_path, sizeof(expanded_key_path)) != 0) {
482 set_error(ERR_INVALID_PATH, "Failed to expand SSH key path: %s", account->ssh_key_path);
483 return -1;
484 }
485
486 /* Verify SSH key file exists and has correct permissions */
487 if (!path_exists(expanded_key_path)) {
488 set_error(ERR_SSH_KEY_NOT_FOUND, "SSH key file not found: %s", expanded_key_path);
489 return -1;
490 }
491
492 /* Build SSH command with security options */
493 if ((size_t)snprintf(ssh_command, sizeof(ssh_command),
494 "ssh -i '%s' -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o LogLevel=ERROR",
495 expanded_key_path) >= sizeof(ssh_command)) {
496 set_error(ERR_INVALID_ARGS, "SSH command too long");
497 return -1;
498 }
499
500 log_debug("Configuring SSH command: %s", ssh_command);
501
502 if (git_set_config_value(GIT_CONFIG_CORE_SSHCOMMAND, ssh_command, scope) != 0) {
503 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set SSH command configuration");
504 return -1;
505 }
506
507 return 0;
508 }
509
510 /* Configure GPG for git operations */
511 int git_configure_gpg(const account_t *account, git_scope_t scope) {
512 if (!account || !account->gpg_enabled || strlen(account->gpg_key_id) == 0) {
513 return 0; /* Nothing to configure */
514 }
515
516 log_debug("Configuring GPG signing key: %s", account->gpg_key_id);
517
518 /* Set signing key */
519 if (git_set_config_value(GIT_CONFIG_USER_SIGNINGKEY, account->gpg_key_id, scope) != 0) {
520 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set GPG signing key");
521 return -1;
522 }
523
524 /* Enable/disable GPG signing */
525 if (git_set_config_value(GIT_CONFIG_COMMIT_GPGSIGN,
526 account->gpg_signing_enabled ? "true" : "false", scope) != 0) {
527 set_error(ERR_GIT_CONFIG_FAILED, "Failed to set GPG signing preference");
528 return -1;
529 }
530
531 return 0;
532 }
533
534 /* Check if current directory is a git repository */
535 bool git_is_repository(void) {
536 char output[256];
537
538 /* Use git rev-parse --git-dir to check for repository */
539 if (execute_git_command("rev-parse --git-dir", output, sizeof(output)) == 0) {
540 return true;
541 }
542
543 return false;
544 }
545
546 /* Get repository root directory */
547 int git_get_repo_root(char *path, size_t path_size) {
548 char output[MAX_PATH_LEN];
549
550 if (!path || path_size == 0) {
551 set_error(ERR_INVALID_ARGS, "Invalid arguments to git_get_repo_root");
552 return -1;
553 }
554
555 if (execute_git_command("rev-parse --show-toplevel", output, sizeof(output)) != 0) {
556 set_error(ERR_GIT_NOT_REPOSITORY, "Not in a git repository");
557 return -1;
558 }
559
560 trim_whitespace(output);
561 safe_strncpy(path, output, path_size);
562
563 return 0;
564 }
565
566 /* Convert scope enum to git config scope string */
567 const char *git_scope_to_flag(git_scope_t scope) {
568 switch (scope) {
569 case GIT_SCOPE_LOCAL: return "--local";
570 case GIT_SCOPE_GLOBAL: return "--global";
571 case GIT_SCOPE_SYSTEM: return "--system";
572 default: return NULL;
573 }
574 }
575
576 /* Internal helper functions */
577
578 /* Execute git command and capture output */
579 static int execute_git_command(const char *args, char *output, size_t output_size) {
580 char command[1024];
581 FILE *pipe;
582
583 if (!args) {
584 return -1;
585 }
586
587 /* Build full git command */
588 if ((size_t)snprintf(command, sizeof(command), "git %s 2>&1", args) >= sizeof(command)) {
589 set_error(ERR_INVALID_ARGS, "Git command too long");
590 return -1;
591 }
592
593 log_debug("Executing git command: %s", command);
594
595 /* Execute command */
596 pipe = popen(command, "r");
597 if (!pipe) {
598 set_system_error(ERR_SYSTEM_COMMAND_FAILED, "Failed to execute git command");
599 return -1;
600 }
601
602 /* Read output if buffer provided */
603 if (output && output_size > 0) {
604 if (!fgets(output, output_size, pipe)) {
605 output[0] = '\0';
606 }
607 }
608
609 int exit_code = pclose(pipe);
610 if (exit_code != 0) {
611 log_debug("Git command failed with exit code: %d", exit_code);
612 return -1;
613 }
614
615 return 0;
616 }
617
618 /* Validate git installation */
619 static int validate_git_installation(void) {
620 char version_output[256];
621
622 /* Check if git is available */
623 if (!command_exists("git")) {
624 set_error(ERR_SYSTEM_REQUIREMENT, "Git is not installed or not in PATH");
625 return -1;
626 }
627
628 /* Get git version */
629 if (execute_git_command("--version", version_output, sizeof(version_output)) != 0) {
630 set_error(ERR_SYSTEM_REQUIREMENT, "Failed to get git version");
631 return -1;
632 }
633
634 log_debug("Git version: %s", version_output);
635
636 /* Basic version check - require git 2.0+ */
637 if (!strstr(version_output, "git version ")) {
638 set_error(ERR_SYSTEM_REQUIREMENT, "Unexpected git version output");
639 return -1;
640 }
641
642 return 0;
643 }
644
645
646 /* Validate git config value for security */
647 static bool is_valid_git_config_value(const char *value) {
648 if (!value) {
649 return false;
650 }
651
652 /* Check for dangerous characters */
653 const char *dangerous_chars = ";|&`$(){}[]";
654 for (const char *p = dangerous_chars; *p; p++) {
655 if (strchr(value, *p)) {
656 return false;
657 }
658 }
659
660 /* Check for control characters */
661 for (const char *p = value; *p; p++) {
662 if (*p < 32 && *p != '\t') {
663 return false;
664 }
665 }
666
667 return true;
668 }
669
670 /* Backup git config if needed */
671 static int backup_git_config_if_needed(git_scope_t scope) {
672 /* TODO: Implement config backup for safety */
673 (void)scope; /* Suppress unused parameter warning */
674 return 0;
675 }
676