| 5371 | } |
| 5372 | |
| 5373 | static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { |
| 5374 | char *project = get_project_arg(args); |
| 5375 | char *base_branch = cbm_mcp_get_string_arg(args, "base_branch"); |
| 5376 | char *since = cbm_mcp_get_string_arg(args, "since"); |
| 5377 | char *scope = cbm_mcp_get_string_arg(args, "scope"); |
| 5378 | int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH); |
| 5379 | depth = clamp_mcp_depth(depth, "detect_changes"); |
| 5380 | |
| 5381 | /* scope: "files" = just changed files, "symbols" = files + symbols (default) */ |
| 5382 | bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0; |
| 5383 | |
| 5384 | /* `since` (e.g. "HEAD~10", "v0.5.0") is the documented diff base but was |
| 5385 | * previously parsed and never used: it takes precedence over base_branch. |
| 5386 | * Route it through base_branch so the shared shell-arg validation and the |
| 5387 | * existing `<base>...HEAD` (three-dot) diff apply unchanged — `since` thus |
| 5388 | * adopts the same merge-base semantics base_branch already uses. */ |
| 5389 | if (since && since[0]) { |
| 5390 | free(base_branch); |
| 5391 | base_branch = since; /* transfer ownership */ |
| 5392 | since = NULL; |
| 5393 | } |
| 5394 | free(since); /* no-op after the swap (since is NULL); frees it otherwise */ |
| 5395 | |
| 5396 | if (!base_branch) { |
| 5397 | base_branch = heap_strdup("main"); |
| 5398 | } |
| 5399 | |
| 5400 | /* Reject shell metacharacters, and a leading '-', in the user-supplied |
| 5401 | * branch name. base_branch is spliced into `git diff --name-only |
| 5402 | * "<base>"...HEAD`; a value starting with '-' would be read by git as an |
| 5403 | * option rather than a ref (e.g. `--output=<path>` writes the diff to an |
| 5404 | * arbitrary file). A real git ref never begins with '-'. */ |
| 5405 | if (!cbm_validate_shell_arg(base_branch) || base_branch[0] == '-') { |
| 5406 | free(project); |
| 5407 | free(base_branch); |
| 5408 | free(scope); |
| 5409 | return cbm_mcp_text_result("base_branch contains invalid characters", true); |
| 5410 | } |
| 5411 | |
| 5412 | char *root_path = get_project_root(srv, project); |
| 5413 | if (!root_path) { |
| 5414 | char *err = build_no_store_error(project); |
| 5415 | char *res = cbm_mcp_text_result(err, true); |
| 5416 | free(err); |
| 5417 | free(project); |
| 5418 | free(base_branch); |
| 5419 | free(scope); |
| 5420 | return res; |
| 5421 | } |
| 5422 | |
| 5423 | if (!validate_search_path_arg(root_path)) { |
| 5424 | free(root_path); |
| 5425 | free(project); |
| 5426 | free(base_branch); |
| 5427 | free(scope); |
| 5428 | return cbm_mcp_text_result("project path contains invalid characters", true); |
| 5429 | } |
| 5430 |
no test coverage detected