| 10290 | } |
| 10291 | |
| 10292 | static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { |
| 10293 | char *project = get_project_arg(args); |
| 10294 | char *base_branch = cbm_mcp_get_string_arg(args, "base_branch"); |
| 10295 | char *since = cbm_mcp_get_string_arg(args, "since"); |
| 10296 | char *scope = cbm_mcp_get_string_arg(args, "scope"); |
| 10297 | int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH); |
| 10298 | depth = clamp_mcp_depth(depth, "detect_changes"); |
| 10299 | |
| 10300 | /* scope: "files" = just changed files, "symbols" = files + symbols (default) */ |
| 10301 | bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0; |
| 10302 | |
| 10303 | /* `since` (e.g. "HEAD~10", "v0.5.0") is the documented diff base but was |
| 10304 | * previously parsed and never used: it takes precedence over base_branch. |
| 10305 | * Route it through base_branch so the shared shell-arg validation and the |
| 10306 | * existing `<base>...HEAD` (three-dot) diff apply unchanged — `since` thus |
| 10307 | * adopts the same merge-base semantics base_branch already uses. */ |
| 10308 | if (since && since[0]) { |
| 10309 | free(base_branch); |
| 10310 | base_branch = since; /* transfer ownership */ |
| 10311 | since = NULL; |
| 10312 | } |
| 10313 | free(since); /* no-op after the swap (since is NULL); frees it otherwise */ |
| 10314 | |
| 10315 | if (!base_branch) { |
| 10316 | base_branch = heap_strdup("main"); |
| 10317 | } |
| 10318 | |
| 10319 | /* Reject shell metacharacters, and a leading '-', in the user-supplied |
| 10320 | * branch name. base_branch is spliced into `git diff --name-only |
| 10321 | * "<base>"...HEAD`; a value starting with '-' would be read by git as an |
| 10322 | * option rather than a ref (e.g. `--output=<path>` writes the diff to an |
| 10323 | * arbitrary file). A real git ref never begins with '-'. */ |
| 10324 | if (!cbm_validate_shell_arg(base_branch) || base_branch[0] == '-' || |
| 10325 | !validate_windows_cmd_interpolation_arg(base_branch)) { |
| 10326 | free(project); |
| 10327 | free(base_branch); |
| 10328 | free(scope); |
| 10329 | return cbm_mcp_text_result("base_branch contains invalid characters", true); |
| 10330 | } |
| 10331 | |
| 10332 | char *root_path = get_project_root(srv, project); |
| 10333 | if (!root_path) { |
| 10334 | char *err = build_no_store_error(project); |
| 10335 | char *res = cbm_mcp_text_result(err, true); |
| 10336 | free(err); |
| 10337 | free(project); |
| 10338 | free(base_branch); |
| 10339 | free(scope); |
| 10340 | return res; |
| 10341 | } |
| 10342 | |
| 10343 | if (!validate_search_path_arg(root_path) || |
| 10344 | !validate_windows_cmd_interpolation_arg(root_path)) { |
| 10345 | free(root_path); |
| 10346 | free(project); |
| 10347 | free(base_branch); |
| 10348 | free(scope); |
| 10349 | return cbm_mcp_text_result("project path contains invalid characters", true); |
no test coverage detected