diff --git a/backend/onyx/connectors/github/connector.py b/backend/onyx/connectors/github/connector.py index 96349d29f..fedea749b 100644 --- a/backend/onyx/connectors/github/connector.py +++ b/backend/onyx/connectors/github/connector.py @@ -162,6 +162,35 @@ class GithubConnector(LoadConnector, PollConnector): _sleep_after_rate_limit_exception(github_client) return self._get_github_repo(github_client, attempt_num + 1) + def _get_github_repos( + self, github_client: Github, attempt_num: int = 0 + ) -> list[Repository.Repository]: + """Get specific repositories based on comma-separated repo_name string.""" + if attempt_num > _MAX_NUM_RATE_LIMIT_RETRIES: + raise RuntimeError( + "Re-tried fetching repos too many times. Something is going wrong with fetching objects from Github" + ) + + try: + repos = [] + # Split repo_name by comma and strip whitespace + repo_names = [name.strip() for name in self.repo_name.split(",")] + + for repo_name in repo_names: + if repo_name: # Skip empty strings + try: + repo = github_client.get_repo(f"{self.repo_owner}/{repo_name}") + repos.append(repo) + except GithubException as e: + logger.warning( + f"Could not fetch repo {self.repo_owner}/{repo_name}: {e}" + ) + + return repos + except RateLimitExceededException: + _sleep_after_rate_limit_exception(github_client) + return self._get_github_repos(github_client, attempt_num + 1) + def _get_all_repos( self, github_client: Github, attempt_num: int = 0 ) -> list[Repository.Repository]: @@ -189,11 +218,17 @@ class GithubConnector(LoadConnector, PollConnector): if self.github_client is None: raise ConnectorMissingCredentialError("GitHub") - repos = ( - [self._get_github_repo(self.github_client)] - if self.repo_name - else self._get_all_repos(self.github_client) - ) + repos = [] + if self.repo_name: + if "," in self.repo_name: + # Multiple repositories specified + repos = self._get_github_repos(self.github_client) + else: + # Single repository (backward compatibility) + repos = [self._get_github_repo(self.github_client)] + else: + # All repositories + repos = self._get_all_repos(self.github_client) for repo in repos: if self.include_prs: @@ -269,10 +304,47 @@ class GithubConnector(LoadConnector, PollConnector): try: if self.repo_name: - test_repo = self.github_client.get_repo( - f"{self.repo_owner}/{self.repo_name}" - ) - test_repo.get_contents("") + if "," in self.repo_name: + # Multiple repositories specified + repo_names = [name.strip() for name in self.repo_name.split(",")] + if not repo_names: + raise ConnectorValidationError( + "Invalid connector settings: No valid repository names provided." + ) + + # Validate at least one repository exists and is accessible + valid_repos = False + validation_errors = [] + + for repo_name in repo_names: + if not repo_name: + continue + + try: + test_repo = self.github_client.get_repo( + f"{self.repo_owner}/{repo_name}" + ) + test_repo.get_contents("") + valid_repos = True + # If at least one repo is valid, we can proceed + break + except GithubException as e: + validation_errors.append( + f"Repository '{repo_name}': {e.data.get('message', str(e))}" + ) + + if not valid_repos: + error_msg = ( + "None of the specified repositories could be accessed: " + ) + error_msg += ", ".join(validation_errors) + raise ConnectorValidationError(error_msg) + else: + # Single repository (backward compatibility) + test_repo = self.github_client.get_repo( + f"{self.repo_owner}/{self.repo_name}" + ) + test_repo.get_contents("") else: # Try to get organization first try: @@ -299,9 +371,14 @@ class GithubConnector(LoadConnector, PollConnector): ) elif e.status == 404: if self.repo_name: - raise ConnectorValidationError( - f"GitHub repository not found with name: {self.repo_owner}/{self.repo_name}" - ) + if "," in self.repo_name: + raise ConnectorValidationError( + f"None of the specified GitHub repositories could be found for owner: {self.repo_owner}" + ) + else: + raise ConnectorValidationError( + f"GitHub repository not found with name: {self.repo_owner}/{self.repo_name}" + ) else: raise ConnectorValidationError( f"GitHub user or organization not found: {self.repo_owner}" diff --git a/web/src/components/admin/connectors/ConnectorTitle.tsx b/web/src/components/admin/connectors/ConnectorTitle.tsx index 525f7f226..f8ede9eb4 100644 --- a/web/src/components/admin/connectors/ConnectorTitle.tsx +++ b/web/src/components/admin/connectors/ConnectorTitle.tsx @@ -41,7 +41,11 @@ export const ConnectorTitle = ({ additionalMetadata.set( "Repo", typedConnector.connector_specific_config.repo_name - ? `${typedConnector.connector_specific_config.repo_owner}/${typedConnector.connector_specific_config.repo_name}` + ? `${typedConnector.connector_specific_config.repo_owner}/${ + typedConnector.connector_specific_config.repo_name.includes(",") + ? "multiple repos" + : typedConnector.connector_specific_config.repo_name + }` : `${typedConnector.connector_specific_config.repo_owner}/*` ); } else if (connector.source === "gitlab") { diff --git a/web/src/lib/connectors/connectors.tsx b/web/src/lib/connectors/connectors.tsx index 99ccde947..52bc4045d 100644 --- a/web/src/lib/connectors/connectors.tsx +++ b/web/src/lib/connectors/connectors.tsx @@ -190,10 +190,12 @@ export const connectorConfigs: Record< fields: [ { type: "text", - query: "Enter the repository name:", - label: "Repository Name", + query: "Enter the repository name(s):", + label: "Repository Name(s)", name: "repo_name", optional: false, + description: + "For multiple repositories, enter comma-separated names (e.g., repo1,repo2,repo3)", }, ], }, @@ -1358,7 +1360,7 @@ export interface WebConfig { export interface GithubConfig { repo_owner: string; - repo_name: string; + repo_name: string; // Can be a comma-separated list of repository names include_prs: boolean; include_issues: boolean; }