Skip to content

Commit 6652c96

Browse files
krid-583Krishna Prasath D
andauthored
repo_search_commits: removing client-side-filters and updating the capabilities of the tool (#1360)
Migrates the `repo_search_commits` tool from the Git REST API (`GET _apis/git/repositories/{repo}/commits`) to the Azure DevOps Search API (`POST almsearch.dev.azure.com/_apis/search/commitSearchResults`). ## GitHub issue number Closes #1327 ## **Associated Risks** None ## ✅ **PR Checklist** - [X] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [X] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [X] Title of the pull request is clear and informative. - [X] 👌 Code hygiene - [ ] 🔭 Telemetry added, updated, or N/A - [X] 📄 Documentation added, updated, or N/A - [X] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Tested the updated unit tests. 1. Test Suites: 18 passed, 18 total 2. Tests: 987 passed, 987 total Manually tested the tool. <img width="1296" height="771" alt="image" src="https://github.com/user-attachments/assets/7dacebaa-3476-4f0e-9e73-5ca227f4e315" /> <img width="1270" height="759" alt="image" src="https://github.com/user-attachments/assets/10241137-866f-4d75-9c18-1babebbbbca8" /> --------- Co-authored-by: Krishna Prasath D <krid@microsoft.com>
1 parent 1522701 commit 6652c96

4 files changed

Lines changed: 346 additions & 387 deletions

File tree

docs/TOOLSET.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,10 @@ Create a new branch in the repository.
277277

278278
### mcp_ado_repo_search_commits
279279

280-
Search for commits in a repository with comprehensive filtering capabilities.
280+
Search for commits across projects and repositories with comprehensive filtering capabilities.
281281

282-
- **Required**: `project`, `repository`
283-
- **Optional**: `author`, `authorEmail`, `commitIds`, `committer`, `committerEmail`, `fromCommit`, `fromDate`, `historySimplificationMode`, `includeLinks`, `includeWorkItems`, `searchText`, `skip`, `toCommit`, `toDate`, `top`, `version`, `versionType`
282+
- **Required**: `searchText`
283+
- **Optional**: `project`, `repository`, `branch`, `author`, `commitStartDate`, `commitEndDate`, `orderBy`, `includeFacets`, `skip`, `top`
284284

285285
### mcp_ado_repo_list_pull_requests_by_repo_or_project
286286

jest.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ module.exports = {
5353
"^(.+)/logger\\.js$": "$1/logger.ts",
5454
"^(.+)/elicitations\\.js$": "$1/elicitations.ts",
5555
"^(.+)/content-safety\\.js$": "$1/content-safety.ts",
56+
"^(.+)/index\\.js$": "$1/index.ts",
5657
},
5758
};

src/tools/repositories.ts

Lines changed: 53 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
GitRef,
88
GitForkRef,
99
PullRequestStatus,
10-
GitQueryCommitsCriteria,
1110
GitVersionType,
1211
GitVersionDescriptor,
1312
GitPullRequestQuery,
@@ -27,7 +26,8 @@ import { z } from "zod";
2726
import { getCurrentUserDetails, getUserIdFromEmail } from "./auth.js";
2827
import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces.js";
2928
import { WebApiTagDefinition } from "azure-devops-node-api/interfaces/CoreInterfaces.js";
30-
import { extractAdoStreamError, getEnumKeys, streamToString } from "../utils.js";
29+
import { extractAdoStreamError, getEnumKeys, streamToString, apiVersion } from "../utils.js";
30+
import { orgName } from "../index.js";
3131

3232
const REPO_TOOLS = {
3333
list_repos_by_project: "repo_list_repos_by_project",
@@ -1736,181 +1736,68 @@ function configureRepoTools(server: McpServer, tokenProvider: () => Promise<stri
17361736
}
17371737
);
17381738

1739-
const gitVersionTypeStrings = Object.values(GitVersionType).filter((value): value is string => typeof value === "string");
1740-
17411739
server.tool(
17421740
REPO_TOOLS.search_commits,
1743-
"Search for commits in a repository with comprehensive filtering capabilities. Supports searching by description/comment text, time range, author, committer, specific commit IDs, and more. This is the unified tool for all commit search operations.",
1741+
"Search for commits in a repository with comprehensive filtering capabilities. Supports searching by description/comment text, time range, author and more.",
17441742
{
1745-
project: z.string().describe("Project name or ID"),
1746-
repository: z.string().describe("Repository name or ID"),
1747-
// Existing parameters
1748-
fromCommit: z.string().optional().describe("Starting commit ID"),
1749-
toCommit: z.string().optional().describe("Ending commit ID"),
1750-
version: z.string().optional().describe("The name of the branch, tag or commit to filter commits by"),
1751-
versionType: z
1752-
.enum(gitVersionTypeStrings as [string, ...string[]])
1743+
searchText: z.string().describe("Keywords to search for in commit messages"),
1744+
project: z
1745+
.union([z.string().transform((value) => [value]), z.array(z.string())])
17531746
.optional()
1754-
.default(GitVersionType[GitVersionType.Branch])
1755-
.describe("The meaning of the version parameter, e.g., branch, tag or commit"),
1756-
skip: z.coerce.number().optional().default(0).describe("Number of commits to skip"),
1757-
top: z.coerce.number().optional().default(10).describe("Maximum number of commits to return"),
1758-
includeLinks: z.boolean().optional().default(false).describe("Include commit links"),
1759-
includeWorkItems: z.boolean().optional().default(false).describe("Include associated work items"),
1760-
// Enhanced search parameters
1761-
searchText: z.string().optional().describe("Search text to filter commits by description/comment. Supports partial matching."),
1762-
author: z.string().optional().describe("Filter commits by author email or display name"),
1763-
authorEmail: z.string().optional().describe("Filter commits by exact author email address"),
1764-
committer: z.string().optional().describe("Filter commits by committer email or display name"),
1765-
committerEmail: z.string().optional().describe("Filter commits by exact committer email address"),
1766-
fromDate: z.string().optional().describe("Filter commits from this date (ISO 8601 format, e.g., '2024-01-01T00:00:00Z')"),
1767-
toDate: z.string().optional().describe("Filter commits to this date (ISO 8601 format, e.g., '2024-12-31T23:59:59Z')"),
1768-
commitIds: z.array(z.string()).optional().describe("Array of specific commit IDs to retrieve. When provided, other filters are ignored except top/skip."),
1769-
historySimplificationMode: z.enum(["FirstParent", "SimplifyMerges", "FullHistory", "FullHistorySimplifyMerges"]).optional().describe("How to simplify the commit history"),
1747+
.describe("The names of the projects to search within. If omitted, searches across all projects in the organization."),
1748+
repository: z.array(z.string()).optional().describe("The names of the repositories to search within. If omitted, searches across all repositories in the specified projects."),
1749+
branch: z.array(z.string()).optional().describe("The names of the repository branches to search within. If omitted, searches across all branches in the specified repositories."),
1750+
author: z.array(z.string()).optional().describe("The names of the commit authors to search for. Only full display names are supported."),
1751+
commitStartDate: z.string().optional().describe("Filter commits from this date (format: 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS')"),
1752+
commitEndDate: z.string().optional().describe("Filter commits up to this date (format: 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS', e.g. '2025-06-19T23:59:59' for full day)"),
1753+
orderBy: z.enum(["ASC", "DESC"]).optional().describe("Sort commits by date: 'ASC' for oldest-first, 'DESC' for newest-first. Defaults to relevance if omitted."),
1754+
includeFacets: z.boolean().default(false).describe("Include facets in the search results"),
1755+
skip: z.coerce.number().default(0).describe("Number of results to skip"),
1756+
top: z.coerce.number().default(10).describe("Maximum number of results to return"),
17701757
},
1771-
async ({
1772-
project,
1773-
repository,
1774-
fromCommit,
1775-
toCommit,
1776-
version,
1777-
versionType,
1778-
skip,
1779-
top,
1780-
includeLinks,
1781-
includeWorkItems,
1782-
searchText,
1783-
author,
1784-
authorEmail,
1785-
committer,
1786-
committerEmail,
1787-
fromDate,
1788-
toDate,
1789-
commitIds,
1790-
historySimplificationMode,
1791-
}) => {
1792-
try {
1793-
const connection = await connectionProvider();
1794-
const gitApi = await connection.getGitApi();
1795-
1796-
// If specific commit IDs are provided, use getCommits with commit ID filtering
1797-
if (commitIds && commitIds.length > 0) {
1798-
const commits = [];
1799-
const batchSize = Math.min(top || 10, commitIds.length);
1800-
const startIndex = skip || 0;
1801-
const endIndex = Math.min(startIndex + batchSize, commitIds.length);
1802-
1803-
// Process commits in the requested range
1804-
const requestedCommitIds = commitIds.slice(startIndex, endIndex);
1805-
1806-
// Use getCommits for each commit ID to maintain consistency
1807-
for (const commitId of requestedCommitIds) {
1808-
try {
1809-
const searchCriteria: GitQueryCommitsCriteria = {
1810-
includeLinks: includeLinks,
1811-
includeWorkItems: includeWorkItems,
1812-
fromCommitId: commitId,
1813-
toCommitId: commitId,
1814-
};
1815-
1816-
const commitResults = await gitApi.getCommits(repository, searchCriteria, project, 0, 1);
1817-
1818-
if (commitResults && commitResults.length > 0) {
1819-
commits.push(commitResults[0]);
1820-
}
1821-
} catch (error) {
1822-
// Log error but continue with other commits
1823-
console.warn(`Failed to retrieve commit ${commitId}: ${error instanceof Error ? error.message : String(error)}`);
1824-
// Add error information to result instead of failing completely
1825-
commits.push({
1826-
commitId: commitId,
1827-
error: `Failed to retrieve: ${error instanceof Error ? error.message : String(error)}`,
1828-
});
1829-
}
1830-
}
1831-
1832-
return {
1833-
content: [{ type: "text", text: JSON.stringify(commits, null, 2) }],
1834-
};
1835-
}
1836-
1837-
const searchCriteria: GitQueryCommitsCriteria = {
1838-
fromCommitId: fromCommit,
1839-
toCommitId: toCommit,
1840-
includeLinks: includeLinks,
1841-
includeWorkItems: includeWorkItems,
1842-
};
1843-
1844-
// Add author filter
1845-
if (author) {
1846-
searchCriteria.author = author;
1847-
}
1848-
1849-
// Add date range filters (ADO API expects ISO string format)
1850-
if (fromDate) {
1851-
searchCriteria.fromDate = fromDate;
1852-
}
1853-
if (toDate) {
1854-
searchCriteria.toDate = toDate;
1855-
}
1856-
1857-
// Add history simplification if specified
1858-
if (historySimplificationMode) {
1859-
// Note: This parameter might not be directly supported by all ADO API versions
1860-
// but we'll include it in the criteria for forward compatibility
1861-
const extendedCriteria = searchCriteria as GitQueryCommitsCriteria & { historySimplificationMode?: string };
1862-
extendedCriteria.historySimplificationMode = historySimplificationMode;
1863-
}
1864-
1865-
if (version) {
1866-
const itemVersion: GitVersionDescriptor = {
1867-
version: version,
1868-
versionType: GitVersionType[versionType as keyof typeof GitVersionType],
1869-
};
1870-
searchCriteria.itemVersion = itemVersion;
1871-
}
1872-
1873-
const commits = await gitApi.getCommits(repository, searchCriteria, project, skip, top);
1874-
1875-
// Additional client-side filtering for enhanced search capabilities
1876-
let filteredCommits = commits;
1758+
async ({ searchText, project, repository, branch, author, commitStartDate, commitEndDate, orderBy, includeFacets, skip, top }) => {
1759+
const accessToken = await tokenProvider();
1760+
const url = `https://almsearch.dev.azure.com/${orgName}/_apis/search/commitSearchResults?api-version=${apiVersion}`;
1761+
1762+
const requestBody: Record<string, unknown> = {
1763+
searchText,
1764+
includeFacets,
1765+
$skip: skip,
1766+
$top: top,
1767+
};
18771768

1878-
// Filter by search text in commit message if not handled by API
1879-
if (searchText && filteredCommits) {
1880-
filteredCommits = filteredCommits.filter((commit) => commit.comment?.toLowerCase().includes(searchText.toLowerCase()));
1881-
}
1769+
const filters: Record<string, string[]> = {};
1770+
if (project && project.length > 0) filters.projectName = project;
1771+
if (repository && repository.length > 0) filters.repositoryName = repository;
1772+
if (branch && branch.length > 0) filters.branchName = branch;
1773+
if (author && author.length > 0) filters.authorName = author;
1774+
if (commitStartDate) filters.commitStartDate = [commitStartDate];
1775+
if (commitEndDate) filters.commitEndDate = [commitEndDate];
18821776

1883-
// Filter by author email if specified
1884-
if (authorEmail && filteredCommits) {
1885-
filteredCommits = filteredCommits.filter((commit) => commit.author?.email?.toLowerCase() === authorEmail.toLowerCase());
1886-
}
1777+
requestBody.filters = filters;
18871778

1888-
// Filter by committer if specified
1889-
if (committer && filteredCommits) {
1890-
filteredCommits = filteredCommits.filter(
1891-
(commit) => commit.committer?.name?.toLowerCase().includes(committer.toLowerCase()) || commit.committer?.email?.toLowerCase().includes(committer.toLowerCase())
1892-
);
1893-
}
1779+
if (orderBy) {
1780+
requestBody.$orderBy = [{ field: "commitDate", sortOrder: orderBy }];
1781+
}
18941782

1895-
// Filter by committer email if specified
1896-
if (committerEmail && filteredCommits) {
1897-
filteredCommits = filteredCommits.filter((commit) => commit.committer?.email?.toLowerCase() === committerEmail.toLowerCase());
1898-
}
1783+
const response = await fetch(url, {
1784+
method: "POST",
1785+
headers: {
1786+
"Content-Type": "application/json",
1787+
"Authorization": `Bearer ${accessToken}`,
1788+
"User-Agent": userAgentProvider(),
1789+
},
1790+
body: JSON.stringify(requestBody),
1791+
});
18991792

1900-
return {
1901-
content: [{ type: "text", text: JSON.stringify(filteredCommits, null, 2) }],
1902-
};
1903-
} catch (error) {
1904-
return {
1905-
content: [
1906-
{
1907-
type: "text",
1908-
text: `Error searching commits: ${error instanceof Error ? error.message : String(error)}`,
1909-
},
1910-
],
1911-
isError: true,
1912-
};
1793+
if (!response.ok) {
1794+
throw new Error(`Azure DevOps Commit Search API error: ${response.status} ${response.statusText}`);
19131795
}
1796+
1797+
const result = await response.text();
1798+
return {
1799+
content: [{ type: "text", text: result }],
1800+
};
19141801
}
19151802
);
19161803

0 commit comments

Comments
 (0)