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