| 10149 | } |
| 10150 | |
| 10151 | static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) { |
| 10152 | char *project = get_project_arg(args); |
| 10153 | char *base_branch = cbm_mcp_get_string_arg(args, "base_branch"); |
| 10154 | char *since = cbm_mcp_get_string_arg(args, "since"); |
| 10155 | char *scope = cbm_mcp_get_string_arg(args, "scope"); |
| 10156 | int depth = cbm_mcp_get_int_arg(args, "depth", MCP_DEFAULT_BFS_DEPTH); |
| 10157 | depth = clamp_mcp_depth(depth, "detect_changes"); |
| 10158 | |
| 10159 | /* scope: "files" = just changed files, "symbols" = files + symbols (default) */ |
| 10160 | bool want_symbols = !scope || strcmp(scope, "symbols") == 0 || strcmp(scope, "impact") == 0; |
| 10161 | |
| 10162 | /* `since` (e.g. "HEAD~10", "v0.5.0") is the documented diff base but was |
| 10163 | * previously parsed and never used: it takes precedence over base_branch. |
| 10164 | * Route it through base_branch so the shared shell-arg validation and the |
| 10165 | * existing `<base>...HEAD` (three-dot) diff apply unchanged — `since` thus |
| 10166 | * adopts the same merge-base semantics base_branch already uses. */ |
| 10167 | if (since && since[0]) { |
| 10168 | free(base_branch); |
| 10169 | base_branch = since; /* transfer ownership */ |
| 10170 | since = NULL; |
| 10171 | } |
| 10172 | free(since); /* no-op after the swap (since is NULL); frees it otherwise */ |
| 10173 | |
| 10174 | if (!base_branch) { |
| 10175 | base_branch = heap_strdup("main"); |
| 10176 | } |
| 10177 | |
| 10178 | /* Reject shell metacharacters, and a leading '-', in the user-supplied |
| 10179 | * branch name. base_branch is spliced into `git diff --name-only |
| 10180 | * "<base>"...HEAD`; a value starting with '-' would be read by git as an |
| 10181 | * option rather than a ref (e.g. `--output=<path>` writes the diff to an |
| 10182 | * arbitrary file). A real git ref never begins with '-'. */ |
| 10183 | if (!cbm_validate_shell_arg(base_branch) || base_branch[0] == '-' || |
| 10184 | !validate_windows_cmd_interpolation_arg(base_branch)) { |
| 10185 | free(project); |
| 10186 | free(base_branch); |
| 10187 | free(scope); |
| 10188 | return cbm_mcp_text_result("base_branch contains invalid characters", true); |
| 10189 | } |
| 10190 | |
| 10191 | char *root_path = get_project_root(srv, project); |
| 10192 | if (!root_path) { |
| 10193 | char *err = build_no_store_error(project); |
| 10194 | char *res = cbm_mcp_text_result(err, true); |
| 10195 | free(err); |
| 10196 | free(project); |
| 10197 | free(base_branch); |
| 10198 | free(scope); |
| 10199 | return res; |
| 10200 | } |
| 10201 | |
| 10202 | if (!validate_search_path_arg(root_path) || |
| 10203 | !validate_windows_cmd_interpolation_arg(root_path)) { |
| 10204 | free(root_path); |
| 10205 | free(project); |
| 10206 | free(base_branch); |
| 10207 | free(scope); |
| 10208 | return cbm_mcp_text_result("project path contains invalid characters", true); |
no test coverage detected