diff --git a/README.md b/README.md index eb3c1a6..f5876d1 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,8 @@ Below is a table outlining the various configuration parameters available for ** | `INCLUDE_LABELS` | Only PRs with mentioned labels will be included in the report. Values should be separated by commas. Example: `bugfix, enhancement` | - | | `INCLUDE_USERS` | Only data for the specified users or teams will be included in the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | | `EXCLUDE_USERS` | Data for the specified users or teams will be excluded from the report. Multiple values should be separated by commas. Example: `dev1, dev2, team1` | - | +| `FILTER_HEAD_BRANCHES` | Only PRs from head branches (source branches) matching the specified regular expression pattern will be included in the report. Example: `^feature` | - | +| `FILTER_BASE_BRANCHES` | Only PRs to base branches (target branches) matching the specified regular expression pattern will be included in the report. Example: `^develop$` | - | | `EXECUTION_OUTCOME` | This parameter allows you to specify the format in which you wish to receive the report. Options include creating a new issue, updating an existing one, obtaining markdown, or JSON. Markdown and JSON will be available in outputs. Can take mulitple values separated by commas: `new-issue`, `markdown`, `collection`, `existing-issue`. This parameter is **required** Example: `existing-issue` | `new-issue` | | `ISSUE_NUMBER` | Issue number to update. Add `existing-issue` to `EXECUTION_OUTCOME` for updating existing issue. The specified issue must already exist at the time the action is executed. This parameter is mandatory if the `EXECUTION_OUTCOME` input includes `existing-issue` value | - | | `ALLOW_ANALYTICS` | Allows sending non-sensitive inputs to mixpanel for better understanding user's needs. Set the value to `false` to disable sending action parameter data | `true` | @@ -305,6 +307,7 @@ Below is a table describing the possible outputs of **pull-request-analytics-act - To hide individual metrics, specify users in the `HIDE_USERS` parameter or leave `total` and GitHub team names in the `SHOW_USERS` parameter. - To avoid a long list of title changes when updating an existing issue, it is recommended to set the title yourself using the `ISSUE_TITLE` parameter. - You can filter pull requests using labels with the `EXCLUDE_LABELS` and `INCLUDE_LABELS` parameters. +- You can filter pull requests by both source (head) and target (base) branches using the `EXCLUDE_HEAD_BRANCHES`, `INCLUDE_HEAD_BRANCHES`, `EXCLUDE_BASE_BRANCHES`, and `INCLUDE_BASE_BRANCHES` parameters. These parameters accept regular expression patterns for flexible filtering. Note that exclude filters always take priority over include filters. ## Troubleshooting diff --git a/action.yml b/action.yml index c224873..89116b7 100644 --- a/action.yml +++ b/action.yml @@ -100,6 +100,12 @@ inputs: EXCLUDE_LABELS: description: "Excludes PRs with mentioned labels. Values should be separated by comma" required: false + FILTER_HEAD_BRANCHES: + description: "Includes only PRs from head branches matching the specified regular expression pattern" + required: false + FILTER_BASE_BRANCHES: + description: "Includes only PRs to base branches matching the specified regular expression pattern" + required: false INCLUDE_USERS: description: "Only data for the specified users will be included in the report. Multiple values should be separated by commas" required: false diff --git a/build/index.js b/build/index.js index 2a57f97..38de7a0 100644 --- a/build/index.js +++ b/build/index.js @@ -78,6 +78,8 @@ const sendActionError = (error) => { USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), }); } else { @@ -138,6 +140,8 @@ const sendActionRun = () => { USE_CHARTS: (0, utils_1.getValueAsIs)("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: (0, utils_1.getValueAsIs)("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!(0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), }); } else { @@ -2419,23 +2423,17 @@ exports.makeComplexRequest = void 0; const utils_1 = __nccwpck_require__(41002); const getDataWithThrottle_1 = __nccwpck_require__(17227); const getPullRequests_1 = __nccwpck_require__(21341); +const utils_2 = __nccwpck_require__(50426); const makeComplexRequest = async (amount = 100, repository, options = { skipComments: true, }) => { const pullRequests = await (0, getPullRequests_1.getPullRequests)(amount, repository); - const pullRequestNumbers = pullRequests - .filter((pr) => { - const excludeLabels = (0, utils_1.getMultipleValuesInput)("EXCLUDE_LABELS"); - const includeLabels = (0, utils_1.getMultipleValuesInput)("INCLUDE_LABELS"); - const isIncludeLabelsCorrect = includeLabels.length > 0 - ? pr.labels.some((label) => includeLabels.includes(label.name)) - : true; - const isExcludeLabelsCorrect = excludeLabels.length > 0 - ? !pr.labels.some((label) => excludeLabels.includes(label.name)) - : true; - return isIncludeLabelsCorrect && isExcludeLabelsCorrect; - }) - .map((item) => item.number); + const pullRequestNumbers = (0, utils_2.filterPRs)(pullRequests, { + excludeLabels: (0, utils_1.getMultipleValuesInput)("EXCLUDE_LABELS"), + includeLabels: (0, utils_1.getMultipleValuesInput)("INCLUDE_LABELS"), + filterHeadBranchesPattern: (0, utils_1.getValueAsIs)("FILTER_HEAD_BRANCHES"), + filterBaseBranchesPattern: (0, utils_1.getValueAsIs)("FILTER_BASE_BRANCHES"), + }); const { PRs, PREvents, PRComments } = await (0, getDataWithThrottle_1.getDataWithThrottle)(pullRequestNumbers, repository, options); const events = PREvents.map((element) => element.status === "fulfilled" ? element.value.data : null); const pullRequestInfo = PRs.map((element) => element.status === "fulfilled" ? element.value.data : null); @@ -2450,6 +2448,40 @@ const makeComplexRequest = async (amount = 100, repository, options = { exports.makeComplexRequest = makeComplexRequest; +/***/ }), + +/***/ 13975: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.filterPRs = void 0; +const filterPRs = (pullRequests, { excludeLabels, includeLabels, filterHeadBranchesPattern, filterBaseBranchesPattern, }) => { + return pullRequests + .filter((pr) => { + const isIncludeLabelsCorrect = includeLabels.length > 0 + ? pr.labels.some((label) => includeLabels.includes(label.name)) + : true; + const isExcludeLabelsCorrect = excludeLabels.length > 0 + ? !pr.labels.some((label) => excludeLabels.includes(label.name)) + : true; + const isFilterHeadBranchesCorrect = filterHeadBranchesPattern.length > 0 + ? new RegExp(filterHeadBranchesPattern).test(pr.head.ref) + : true; + const isFilterBaseBranchesCorrect = filterBaseBranchesPattern.length > 0 + ? new RegExp(filterBaseBranchesPattern).test(pr.base.ref) + : true; + return (isIncludeLabelsCorrect && + isExcludeLabelsCorrect && + isFilterHeadBranchesCorrect && + isFilterBaseBranchesCorrect); + }) + .map((item) => item.number); +}; +exports.filterPRs = filterPRs; + + /***/ }), /***/ 57288: @@ -2513,11 +2545,13 @@ exports.getReportDates = getReportDates; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getOwnersRepositories = exports.getReportDates = void 0; +exports.filterPRs = exports.getOwnersRepositories = exports.getReportDates = void 0; var getReportDates_1 = __nccwpck_require__(30183); Object.defineProperty(exports, "getReportDates", ({ enumerable: true, get: function () { return getReportDates_1.getReportDates; } })); var getOwnersRepositories_1 = __nccwpck_require__(57288); Object.defineProperty(exports, "getOwnersRepositories", ({ enumerable: true, get: function () { return getOwnersRepositories_1.getOwnersRepositories; } })); +var filterPRs_1 = __nccwpck_require__(13975); +Object.defineProperty(exports, "filterPRs", ({ enumerable: true, get: function () { return filterPRs_1.filterPRs; } })); /***/ }), @@ -2917,6 +2951,8 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", + "FILTER_HEAD_BRANCHES", + "FILTER_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME", diff --git a/package-lock.json b/package-lock.json index 364e986..dd6a925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "license": "MIT", "dependencies": { "@actions/core": "^1.10.1", diff --git a/package.json b/package.json index 8b33c50..8e81710 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pull-request-analytics-action", - "version": "4.8.1", + "version": "4.9.0", "description": "Generates detailed PR analytics reports within GitHub, focusing on review efficiency and team performance.", "main": "build/index.js", "scripts": { diff --git a/src/analytics/sendActionError.ts b/src/analytics/sendActionError.ts index be01bc7..31caac4 100644 --- a/src/analytics/sendActionError.ts +++ b/src/analytics/sendActionError.ts @@ -44,6 +44,8 @@ export const sendActionError = (error: Error) => { USE_CHARTS: getValueAsIs("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!getValueAsIs("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!getValueAsIs("FILTER_BASE_BRANCHES"), }); } else { mixpanel.track("Anonymous action error", { distinct_id: "anonymous" }); diff --git a/src/analytics/sendActionRun.ts b/src/analytics/sendActionRun.ts index 4516c8a..79dd58b 100644 --- a/src/analytics/sendActionRun.ts +++ b/src/analytics/sendActionRun.ts @@ -47,6 +47,8 @@ export const sendActionRun = () => { USE_CHARTS: getValueAsIs("USE_CHARTS"), SHOW_CORRELATION_GRAPHS: getValueAsIs("SHOW_CORRELATION_GRAPHS"), SHOW_ACTIVITY_TIME_GRAPHS: getValueAsIs("SHOW_ACTIVITY_TIME_GRAPHS"), + FILTER_HEAD_BRANCHES: !!getValueAsIs("FILTER_HEAD_BRANCHES"), + FILTER_BASE_BRANCHES: !!getValueAsIs("FILTER_BASE_BRANCHES"), }); } else { mixpanel.track("Anomymous action run", { distinct_id: "anonymous" }); diff --git a/src/requests/makeComplexRequest.ts b/src/requests/makeComplexRequest.ts index 243dd40..4e540e8 100644 --- a/src/requests/makeComplexRequest.ts +++ b/src/requests/makeComplexRequest.ts @@ -1,7 +1,8 @@ -import { getMultipleValuesInput } from "../common/utils"; +import { getMultipleValuesInput, getValueAsIs } from "../common/utils"; import { getDataWithThrottle } from "./getDataWithThrottle"; import { getPullRequests } from "./getPullRequests"; import { Options, Repository } from "./types"; +import { filterPRs } from "./utils"; export const makeComplexRequest = async ( amount: number = 100, @@ -12,21 +13,12 @@ export const makeComplexRequest = async ( ) => { const pullRequests = await getPullRequests(amount, repository); - const pullRequestNumbers = pullRequests - .filter((pr) => { - const excludeLabels = getMultipleValuesInput("EXCLUDE_LABELS"); - const includeLabels = getMultipleValuesInput("INCLUDE_LABELS"); - const isIncludeLabelsCorrect = - includeLabels.length > 0 - ? pr.labels.some((label) => includeLabels.includes(label.name)) - : true; - const isExcludeLabelsCorrect = - excludeLabels.length > 0 - ? !pr.labels.some((label) => excludeLabels.includes(label.name)) - : true; - return isIncludeLabelsCorrect && isExcludeLabelsCorrect; - }) - .map((item) => item.number); + const pullRequestNumbers = filterPRs(pullRequests, { + excludeLabels: getMultipleValuesInput("EXCLUDE_LABELS"), + includeLabels: getMultipleValuesInput("INCLUDE_LABELS"), + filterHeadBranchesPattern: getValueAsIs("FILTER_HEAD_BRANCHES"), + filterBaseBranchesPattern: getValueAsIs("FILTER_BASE_BRANCHES"), + }); const { PRs, PREvents, PRComments } = await getDataWithThrottle( pullRequestNumbers, diff --git a/src/requests/utils/filterPRs.mock.ts b/src/requests/utils/filterPRs.mock.ts new file mode 100644 index 0000000..c7f66ba --- /dev/null +++ b/src/requests/utils/filterPRs.mock.ts @@ -0,0 +1,70 @@ +// Mock data: 10 PRs with different branches and labels +export const mockPullRequests = [ + // PRs with fix/ branch + { + number: 1, + head: { ref: "fix/authentication-bug" }, + base: { ref: "main" }, + labels: [{ name: "bug" }, { name: "enhancement" }], + }, + { + number: 2, + head: { ref: "fix/ui-glitch" }, + base: { ref: "develop" }, + labels: [{ name: "ui-kit" }, { name: "bug" }], + }, + { + number: 3, + head: { ref: "fix/validation-error" }, + base: { ref: "main" }, + labels: [], + }, + + // PRs with feature/ branch + { + number: 4, + head: { ref: "feature/user-dashboard" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }, { name: "ui-kit" }], + }, + { + number: 5, + head: { ref: "feature/export-functionality" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }], + }, + { + number: 6, + head: { ref: "feature/user-profile-page" }, + base: { ref: "main" }, + labels: [{ name: "enhancement" }, { name: "docs" }], + }, + + // PRs with refactor/ branch + { + number: 7, + head: { ref: "refactor/authentication-module" }, + base: { ref: "develop" }, + labels: [{ name: "enhancement" }], + }, + { + number: 8, + head: { ref: "refactor/api-endpoints" }, + base: { ref: "main" }, + labels: [], + }, + + // PRs with cursor/ branch + { + number: 9, + head: { ref: "cursor/code-improvements" }, + base: { ref: "main" }, + labels: [{ name: "enhancement" }], + }, + { + number: 10, + head: { ref: "cursor/ui-fixes" }, + base: { ref: "develop" }, + labels: [{ name: "ui-kit" }, { name: "bug" }], + }, +]; diff --git a/src/requests/utils/filterPRs.spec.ts b/src/requests/utils/filterPRs.spec.ts new file mode 100644 index 0000000..822c519 --- /dev/null +++ b/src/requests/utils/filterPRs.spec.ts @@ -0,0 +1,282 @@ +import { filterPRs } from "./filterPRs"; +import { mockPullRequests } from "./filterPRs.mock"; + +describe("filterPRs", () => { + describe("Label filtering", () => { + it("should return all PRs when no filters are provided", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it("should filter PRs by include labels (bug)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug"], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 10]); + }); + + it("should filter PRs by multiple include labels (bug OR enhancement)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug", "enhancement"], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + // PRs that have at least one of the labels + expect(result).toEqual([1, 2, 4, 5, 6, 7, 9, 10]); + }); + + it("should filter PRs by exclude labels (bug)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: [], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + // All PRs except those with "bug" label + expect(result).toEqual([3, 4, 5, 6, 7, 8, 9]); + }); + + it("should filter PRs by multiple exclude labels (bug AND ui-kit)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug", "ui-kit"], + includeLabels: [], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + // PRs that don't have any of the excluded labels + expect(result).toEqual([3, 5, 6, 7, 8, 9]); + }); + + it("should filter PRs with both include and exclude labels", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: ["enhancement"], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + // PRs that have "enhancement" but not "bug" + expect(result).toEqual([4, 5, 6, 7, 9]); + }); + + it("should return only PRs without any labels when including non-existent label", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["non-existent-label"], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + expect(result).toEqual([]); + }); + + it("should return all PRs when excluding non-existent label", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["non-existent-label"], + includeLabels: [], + filterBaseBranchesPattern: "", + filterHeadBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + }); + + describe("Branch filtering", () => { + it("should filter PRs by include branch pattern (fix/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^fix/", + filterBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3]); + }); + + it("should filter PRs by multiple branch patterns (fix/ OR feature/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^(fix|feature)/", + filterBaseBranchesPattern: "", + }); + + expect(result).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should filter PRs by exclude branch pattern (fix/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^(?!fix/)", + filterBaseBranchesPattern: "", + }); + + // All PRs except those starting with "fix/" + expect(result).toEqual([4, 5, 6, 7, 8, 9, 10]); + }); + + it("should filter PRs by multiple exclude branch patterns (cursor/ AND refactor/)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^(?!cursor|refactor).*", + filterBaseBranchesPattern: "", + }); + + // PRs that don't start with "cursor/" or "refactor/" + expect(result).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it("should filter PRs with both include and exclude branch patterns", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^feature/(?!.*dashboard).*", + filterBaseBranchesPattern: "", + }); + + // PRs that start with "feature/" but don't contain "dashboard" + expect(result).toEqual([5, 6]); + }); + + it("should filter PRs by branch pattern containing specific keyword", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "ui", + filterBaseBranchesPattern: "", + }); + + // PRs with "ui" in branch name + expect(result).toEqual([2, 10]); + }); + }); + + describe("Combined filtering (labels + branches)", () => { + it("should filter PRs by include label and include branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["bug"], + filterHeadBranchesPattern: "^fix/", + filterBaseBranchesPattern: "", + }); + + // PRs with "bug" label AND starting with "fix/" + expect(result).toEqual([1, 2]); + }); + + it("should filter PRs by include label and exclude branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["ui-kit"], + filterHeadBranchesPattern: "^(?!cursor/).*", + filterBaseBranchesPattern: "", + }); + + // PRs with "ui-kit" label AND not starting with "cursor/" + expect(result).toEqual([2, 4]); + }); + + it("should filter PRs by exclude label and include branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: [], + filterHeadBranchesPattern: "^feature/", + filterBaseBranchesPattern: "", + }); + + // PRs starting with "feature/" AND not having "bug" label + expect(result).toEqual([4, 5, 6]); + }); + + it("should filter PRs by exclude label and exclude branch pattern", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["docs"], + includeLabels: [], + filterHeadBranchesPattern: "^(?!fix/).*", + filterBaseBranchesPattern: "", + }); + + // PRs not starting with "fix/" AND not having "docs" label + expect(result).toEqual([4, 5, 7, 8, 9, 10]); + }); + + it("should filter PRs with all filters applied", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: ["bug"], + includeLabels: ["enhancement"], + filterHeadBranchesPattern: "^(feature|refactor)/(?!cursor/).*", + filterBaseBranchesPattern: "", + }); + + // PRs with "enhancement" label, without "bug" label, + // starting with "feature/" or "refactor/", and not starting with "cursor/" + expect(result).toEqual([4, 5, 6, 7]); + }); + + it("should return empty array when filters exclude all PRs", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: ["non-existent"], + filterHeadBranchesPattern: "^non-existent/", + filterBaseBranchesPattern: "", + }); + + expect(result).toEqual([]); + }); + }); + + describe("Base branch filtering", () => { + it("should filter PRs by include base branch pattern (main)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "^main$", + }); + + // PRs with base branch = main + expect(result).toEqual([1, 3, 6, 8, 9]); + }); + + it("should filter PRs by exclude base branch pattern (main)", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "", + filterBaseBranchesPattern: "^(?!main$).*", + }); + + // All PRs except those with base branch = main + expect(result).toEqual([2, 4, 5, 7, 10]); + }); + + it("should filter PRs by both head and base branch patterns", () => { + const result = filterPRs(mockPullRequests, { + excludeLabels: [], + includeLabels: [], + filterHeadBranchesPattern: "^feature/", + filterBaseBranchesPattern: "^develop$", + }); + + // PRs starting with "feature/" OR with base branch = develop + expect(result).toEqual([4, 5]); + }); + }); +}); diff --git a/src/requests/utils/filterPRs.ts b/src/requests/utils/filterPRs.ts new file mode 100644 index 0000000..c349d56 --- /dev/null +++ b/src/requests/utils/filterPRs.ts @@ -0,0 +1,48 @@ +export const filterPRs = ( + pullRequests: { + labels: { name: string }[]; + head: { ref: string }; + base: { ref: string }; + number: number; + }[], + { + excludeLabels, + includeLabels, + filterHeadBranchesPattern, + filterBaseBranchesPattern, + }: { + excludeLabels: string[]; + includeLabels: string[]; + filterHeadBranchesPattern: string; + filterBaseBranchesPattern: string; + } +) => { + return pullRequests + .filter((pr) => { + const isIncludeLabelsCorrect = + includeLabels.length > 0 + ? pr.labels.some((label) => includeLabels.includes(label.name)) + : true; + const isExcludeLabelsCorrect = + excludeLabels.length > 0 + ? !pr.labels.some((label) => excludeLabels.includes(label.name)) + : true; + + const isFilterHeadBranchesCorrect = + filterHeadBranchesPattern.length > 0 + ? new RegExp(filterHeadBranchesPattern).test(pr.head.ref) + : true; + const isFilterBaseBranchesCorrect = + filterBaseBranchesPattern.length > 0 + ? new RegExp(filterBaseBranchesPattern).test(pr.base.ref) + : true; + + return ( + isIncludeLabelsCorrect && + isExcludeLabelsCorrect && + isFilterHeadBranchesCorrect && + isFilterBaseBranchesCorrect + ); + }) + .map((item) => item.number); +}; diff --git a/src/requests/utils/index.ts b/src/requests/utils/index.ts index ee8b8b6..f1808a8 100644 --- a/src/requests/utils/index.ts +++ b/src/requests/utils/index.ts @@ -1,2 +1,3 @@ export { getReportDates } from "./getReportDates"; export { getOwnersRepositories } from "./getOwnersRepositories"; +export { filterPRs } from "./filterPRs"; diff --git a/src/view/utils/createConfigParamsCode.ts b/src/view/utils/createConfigParamsCode.ts index 1d5c230..19de44b 100644 --- a/src/view/utils/createConfigParamsCode.ts +++ b/src/view/utils/createConfigParamsCode.ts @@ -36,6 +36,8 @@ ${[ "USE_CHARTS", "INCLUDE_LABELS", "EXCLUDE_LABELS", + "FILTER_HEAD_BRANCHES", + "FILTER_BASE_BRANCHES", "INCLUDE_USERS", "EXCLUDE_USERS", "EXECUTION_OUTCOME",