diff --git a/api/data_pipeline.py b/api/data_pipeline.py index a8c0b6610..26dba91ed 100644 --- a/api/data_pipeline.py +++ b/api/data_pipeline.py @@ -55,7 +55,7 @@ def count_tokens(text: str, is_ollama_embedder: bool = None) -> int: # Rough approximation: 4 characters per token return len(text) // 4 -def download_repo(repo_url: str, local_path: str, type: str = "github", access_token: str = None) -> str: +def download_repo(repo_url: str, local_path: str, type: str = "github", access_token: str = None, branch: str = None) -> str: """ Downloads a Git repository (GitHub, GitLab, or Bitbucket) to a specified local path. @@ -63,6 +63,7 @@ def download_repo(repo_url: str, local_path: str, type: str = "github", access_t repo_url (str): The URL of the Git repository to clone. local_path (str): The local directory where the repository will be cloned. access_token (str, optional): Access token for private repositories. + branch (str, optional): Branch to clone. Returns: str: The output message from the `git` command. @@ -105,10 +106,14 @@ def download_repo(repo_url: str, local_path: str, type: str = "github", access_t logger.info("Using access token for authentication") # Clone the repository - logger.info(f"Cloning repository from {repo_url} to {local_path}") + logger.info(f"Cloning repository from {repo_url} to {local_path} (branch: {branch or 'default'})") # We use repo_url in the log to avoid exposing the token in logs + clone_cmd = ["git", "clone", "--depth=1", "--single-branch"] + if branch: + clone_cmd += ["--branch", branch] + clone_cmd += [clone_url, local_path] result = subprocess.run( - ["git", "clone", "--depth=1", "--single-branch", clone_url, local_path], + clone_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -413,7 +418,7 @@ def transform_documents_and_save_to_db( db.save_state(filepath=db_path) return db -def get_github_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: +def get_github_file_content(repo_url: str, file_path: str, access_token: str = None, branch: str = None) -> str: """ Retrieves the content of a file from a GitHub repository using the GitHub API. Supports both public GitHub (github.com) and GitHub Enterprise (custom domains). @@ -423,6 +428,7 @@ def get_github_file_content(repo_url: str, file_path: str, access_token: str = N (e.g., "https://github.com/username/repo" or "https://github.company.com/username/repo") file_path (str): The path to the file within the repository (e.g., "src/main.py") access_token (str, optional): GitHub personal access token for private repositories + branch (str, optional): Branch name or tag to fetch from Returns: str: The content of the file as a string @@ -455,6 +461,8 @@ def get_github_file_content(repo_url: str, file_path: str, access_token: str = N # Use GitHub API to get file content # The API endpoint for getting file content is: /repos/{owner}/{repo}/contents/{path} api_url = f"{api_base}/repos/{owner}/{repo}/contents/{file_path}" + if branch: + api_url += f"?ref={branch}" # Fetch file content from GitHub API headers = {} @@ -490,7 +498,7 @@ def get_github_file_content(repo_url: str, file_path: str, access_token: str = N except Exception as e: raise ValueError(f"Failed to get file content: {str(e)}") -def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: +def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = None, branch: str = None) -> str: """ Retrieves the content of a file from a GitLab repository (cloud or self-hosted). @@ -498,6 +506,7 @@ def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = N repo_url (str): The GitLab repo URL (e.g., "https://gitlab.com/username/repo" or "http://localhost/group/project") file_path (str): File path within the repository (e.g., "src/main.py") access_token (str, optional): GitLab personal access token + branch (str, optional): Branch name or tag to fetch from Returns: str: File content @@ -525,27 +534,29 @@ def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = N # Encode file path encoded_file_path = quote(file_path, safe='') - # Try to get the default branch from the project info - default_branch = None - try: - project_info_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}" - project_headers = {} - if access_token: - project_headers["PRIVATE-TOKEN"] = access_token - - project_response = requests.get(project_info_url, headers=project_headers) - if project_response.status_code == 200: - project_data = project_response.json() - default_branch = project_data.get('default_branch', 'main') - logger.info(f"Found default branch: {default_branch}") - else: - logger.warning(f"Could not fetch project info, using 'main' as default branch") - default_branch = 'main' - except Exception as e: - logger.warning(f"Error fetching project info: {e}, using 'main' as default branch") - default_branch = 'main' + # Determine branch to use + ref_to_use = branch + if not ref_to_use: + # Try to get the default branch from the project info + try: + project_info_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}" + project_headers = {} + if access_token: + project_headers["PRIVATE-TOKEN"] = access_token + + project_response = requests.get(project_info_url, headers=project_headers) + if project_response.status_code == 200: + project_data = project_response.json() + ref_to_use = project_data.get('default_branch', 'main') + logger.info(f"Found default branch: {ref_to_use}") + else: + logger.warning(f"Could not fetch project info, using 'main' as default branch") + ref_to_use = 'main' + except Exception as e: + logger.warning(f"Error fetching project info: {e}, using 'main' as default branch") + ref_to_use = 'main' - api_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}/repository/files/{encoded_file_path}/raw?ref={default_branch}" + api_url = f"{gitlab_domain}/api/v4/projects/{encoded_project_path}/repository/files/{encoded_file_path}/raw?ref={ref_to_use}" # Fetch file content from GitLab API headers = {} if access_token: @@ -572,7 +583,7 @@ def get_gitlab_file_content(repo_url: str, file_path: str, access_token: str = N except Exception as e: raise ValueError(f"Failed to get file content: {str(e)}") -def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str = None) -> str: +def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str = None, branch: str = None) -> str: """ Retrieves the content of a file from a Bitbucket repository using the Bitbucket API. @@ -580,6 +591,7 @@ def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str repo_url (str): The URL of the Bitbucket repository (e.g., "https://bitbucket.org/username/repo") file_path (str): The path to the file within the repository (e.g., "src/main.py") access_token (str, optional): Bitbucket personal access token for private repositories + branch (str, optional): Branch name or tag to fetch from Returns: str: The content of the file as a string @@ -596,29 +608,31 @@ def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str owner = parts[-2] repo = parts[-1].replace(".git", "") - # Try to get the default branch from the repository info - default_branch = None - try: - repo_info_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}" - repo_headers = {} - if access_token: - repo_headers["Authorization"] = f"Bearer {access_token}" - - repo_response = requests.get(repo_info_url, headers=repo_headers) - if repo_response.status_code == 200: - repo_data = repo_response.json() - default_branch = repo_data.get('mainbranch', {}).get('name', 'main') - logger.info(f"Found default branch: {default_branch}") - else: - logger.warning(f"Could not fetch repository info, using 'main' as default branch") - default_branch = 'main' - except Exception as e: - logger.warning(f"Error fetching repository info: {e}, using 'main' as default branch") - default_branch = 'main' + # Determine branch to use + branch_to_use = branch + if not branch_to_use: + # Try to get the default branch from the repository info + try: + repo_info_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}" + repo_headers = {} + if access_token: + repo_headers["Authorization"] = f"Bearer {access_token}" + + repo_response = requests.get(repo_info_url, headers=repo_headers) + if repo_response.status_code == 200: + repo_data = repo_response.json() + branch_to_use = repo_data.get('mainbranch', {}).get('name', 'main') + logger.info(f"Found default branch: {branch_to_use}") + else: + logger.warning(f"Could not fetch repository info, using 'main' as default branch") + branch_to_use = 'main' + except Exception as e: + logger.warning(f"Error fetching repository info: {e}, using 'main' as default branch") + branch_to_use = 'main' # Use Bitbucket API to get file content # The API endpoint for getting file content is: /2.0/repositories/{owner}/{repo}/src/{branch}/{path} - api_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}/src/{default_branch}/{file_path}" + api_url = f"https://api.bitbucket.org/2.0/repositories/{owner}/{repo}/src/{branch_to_use}/{file_path}" # Fetch file content from Bitbucket API headers = {} @@ -648,14 +662,15 @@ def get_bitbucket_file_content(repo_url: str, file_path: str, access_token: str raise ValueError(f"Failed to get file content: {str(e)}") -def get_file_content(repo_url: str, file_path: str, type: str = "github", access_token: str = None) -> str: +def get_file_content(repo_url: str, file_path: str, type: str = "github", access_token: str = None, branch: str = None) -> str: """ - Retrieves the content of a file from a Git repository (GitHub or GitLab). + Retrieves the content of a file from a Git repository (GitHub, GitLab, or Bitbucket). Args: repo_url (str): The URL of the repository file_path (str): The path to the file within the repository access_token (str, optional): Access token for private repositories + branch (str, optional): Branch name to read from Returns: str: The content of the file as a string @@ -664,13 +679,13 @@ def get_file_content(repo_url: str, file_path: str, type: str = "github", access ValueError: If the file cannot be fetched or if the URL is not valid """ if type == "github": - return get_github_file_content(repo_url, file_path, access_token) + return get_github_file_content(repo_url, file_path, access_token, branch) elif type == "gitlab": - return get_gitlab_file_content(repo_url, file_path, access_token) + return get_gitlab_file_content(repo_url, file_path, access_token, branch) elif type == "bitbucket": - return get_bitbucket_file_content(repo_url, file_path, access_token) + return get_bitbucket_file_content(repo_url, file_path, access_token, branch) else: - raise ValueError("Unsupported repository URL. Only GitHub and GitLab are supported.") + raise ValueError("Unsupported repository URL. Only GitHub, GitLab and Bitbucket are supported.") class DatabaseManager: """ @@ -684,7 +699,7 @@ def __init__(self): def prepare_database(self, repo_url_or_path: str, type: str = "github", access_token: str = None, is_ollama_embedder: bool = None, excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None) -> List[Document]: + included_dirs: List[str] = None, included_files: List[str] = None, branch: str = None) -> List[Document]: """ Create a new database from the repository. @@ -702,7 +717,7 @@ def prepare_database(self, repo_url_or_path: str, type: str = "github", access_t List[Document]: List of Document objects """ self.reset_database() - self._create_repo(repo_url_or_path, type, access_token) + self._create_repo(repo_url_or_path, type, access_token, branch) return self.prepare_db_index(is_ollama_embedder=is_ollama_embedder, excluded_dirs=excluded_dirs, excluded_files=excluded_files, included_dirs=included_dirs, included_files=included_files) @@ -714,7 +729,7 @@ def reset_database(self): self.repo_url_or_path = None self.repo_paths = None - def _extract_repo_name_from_url(self, repo_url_or_path: str, repo_type: str) -> str: + def _extract_repo_name_from_url(self, repo_url_or_path: str, repo_type: str, branch: str = None) -> str: # Extract owner and repo name to create unique identifier url_parts = repo_url_or_path.rstrip('/').split('/') @@ -727,18 +742,23 @@ def _extract_repo_name_from_url(self, repo_url_or_path: str, repo_type: str) -> repo_name = f"{owner}_{repo}" else: repo_name = url_parts[-1].replace(".git", "") + # Append branch suffix if provided + if branch: + safe_branch = re.sub(r"[^A-Za-z0-9._-]+", "_", branch) + repo_name = f"{repo_name}__{safe_branch}" return repo_name - def _create_repo(self, repo_url_or_path: str, repo_type: str = "github", access_token: str = None) -> None: + def _create_repo(self, repo_url_or_path: str, repo_type: str = "github", access_token: str = None, branch: str = None) -> None: """ Download and prepare all paths. Paths: - ~/.adalflow/repos/{owner}_{repo_name} (for url, local path will be the same) - ~/.adalflow/databases/{owner}_{repo_name}.pkl + ~/.adalflow/repos/{owner}_{repo_name}[__branch] (for url, local path will be the same) + ~/.adalflow/databases/{owner}_{repo_name}[__branch].pkl Args: repo_url_or_path (str): The URL or local path of the repository access_token (str, optional): Access token for private repositories + branch (str, optional): Branch to use """ logger.info(f"Preparing repo storage for {repo_url_or_path}...") @@ -749,7 +769,7 @@ def _create_repo(self, repo_url_or_path: str, repo_type: str = "github", access_ # url if repo_url_or_path.startswith("https://") or repo_url_or_path.startswith("http://"): # Extract the repository name from the URL - repo_name = self._extract_repo_name_from_url(repo_url_or_path, repo_type) + repo_name = self._extract_repo_name_from_url(repo_url_or_path, repo_type, branch) logger.info(f"Extracted repo name: {repo_name}") save_repo_dir = os.path.join(root_path, "repos", repo_name) @@ -757,11 +777,14 @@ def _create_repo(self, repo_url_or_path: str, repo_type: str = "github", access_ # Check if the repository directory already exists and is not empty if not (os.path.exists(save_repo_dir) and os.listdir(save_repo_dir)): # Only download if the repository doesn't exist or is empty - download_repo(repo_url_or_path, save_repo_dir, repo_type, access_token) + download_repo(repo_url_or_path, save_repo_dir, repo_type, access_token, branch) else: logger.info(f"Repository already exists at {save_repo_dir}. Using existing repository.") else: # local path repo_name = os.path.basename(repo_url_or_path) + if branch: + safe_branch = re.sub(r"[^A-Za-z0-9._-]+", "_", branch) + repo_name = f"{repo_name}__{safe_branch}" save_repo_dir = repo_url_or_path save_db_file = os.path.join(root_path, "databases", f"{repo_name}.pkl") @@ -826,7 +849,7 @@ def prepare_db_index(self, is_ollama_embedder: bool = None, excluded_dirs: List[ logger.info(f"Total transformed documents: {len(transformed_docs)}") return transformed_docs - def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_token: str = None): + def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_token: str = None, branch: str = None): """ Prepare the retriever for a repository. This is a compatibility method for the isolated API. @@ -834,8 +857,9 @@ def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_ Args: repo_url_or_path (str): The URL or local path of the repository access_token (str, optional): Access token for private repositories + branch (str, optional): Branch to use Returns: List[Document]: List of Document objects """ - return self.prepare_database(repo_url_or_path, type, access_token) + return self.prepare_database(repo_url_or_path, type, access_token, branch=branch) diff --git a/api/rag.py b/api/rag.py index 3ff916988..8d194212a 100644 --- a/api/rag.py +++ b/api/rag.py @@ -343,7 +343,7 @@ def _validate_and_filter_embeddings(self, documents: List) -> List: def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_token: str = None, excluded_dirs: List[str] = None, excluded_files: List[str] = None, - included_dirs: List[str] = None, included_files: List[str] = None): + included_dirs: List[str] = None, included_files: List[str] = None, branch: str = None): """ Prepare the retriever for a repository. Will load database from local storage if available. @@ -355,6 +355,7 @@ def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_ excluded_files: Optional list of file patterns to exclude from processing included_dirs: Optional list of directories to include exclusively included_files: Optional list of file patterns to include exclusively + branch: Optional branch to use for cloning and indexing """ self.initialize_db_manager() self.repo_url_or_path = repo_url_or_path @@ -366,7 +367,8 @@ def prepare_retriever(self, repo_url_or_path: str, type: str = "github", access_ excluded_dirs=excluded_dirs, excluded_files=excluded_files, included_dirs=included_dirs, - included_files=included_files + included_files=included_files, + branch=branch ) logger.info(f"Loaded {len(self.transformed_docs)} documents for retrieval") diff --git a/api/websocket_wiki.py b/api/websocket_wiki.py index 2a7cce9e3..05d2c701d 100644 --- a/api/websocket_wiki.py +++ b/api/websocket_wiki.py @@ -38,6 +38,7 @@ class ChatCompletionRequest(BaseModel): filePath: Optional[str] = Field(None, description="Optional path to a file in the repository to include in the prompt") token: Optional[str] = Field(None, description="Personal access token for private repositories") type: Optional[str] = Field("github", description="Type of repository (e.g., 'github', 'gitlab', 'bitbucket')") + branch: Optional[str] = Field(None, description="Branch name to use for retrieval and file content") # model parameters provider: str = Field("google", description="Model provider (google, openai, openrouter, ollama, azure)") @@ -95,7 +96,7 @@ async def handle_websocket_chat(websocket: WebSocket): included_files = [unquote(file_pattern) for file_pattern in request.included_files.split('\n') if file_pattern.strip()] logger.info(f"Using custom included files: {included_files}") - request_rag.prepare_retriever(request.repo_url, request.type, request.token, excluded_dirs, excluded_files, included_dirs, included_files) + request_rag.prepare_retriever(request.repo_url, request.type, request.token, excluded_dirs, excluded_files, included_dirs, included_files, branch=request.branch) logger.info(f"Retriever prepared for {request.repo_url}") except ValueError as e: if "No valid documents with embeddings found" in str(e): @@ -391,8 +392,8 @@ async def handle_websocket_chat(websocket: WebSocket): file_content = "" if request.filePath: try: - file_content = get_file_content(request.repo_url, request.filePath, request.type, request.token) - logger.info(f"Successfully retrieved content for file: {request.filePath}") + file_content = get_file_content(request.repo_url, request.filePath, request.type, request.token, request.branch) + logger.info(f"Successfully retrieved content for file: {request.filePath} (branch: {request.branch or 'default'})") except Exception as e: logger.error(f"Error retrieving file content: {str(e)}") # Continue without file content if there's an error diff --git a/src/app/[owner]/[repo]/page.tsx b/src/app/[owner]/[repo]/page.tsx index 80e403b8f..cf3b937eb 100644 --- a/src/app/[owner]/[repo]/page.tsx +++ b/src/app/[owner]/[repo]/page.tsx @@ -106,7 +106,8 @@ const addTokensToRequestBody = ( excludedDirs?: string, excludedFiles?: string, includedDirs?: string, - includedFiles?: string + includedFiles?: string, + branch?: string ): void => { if (token !== '') { requestBody.token = token; @@ -134,7 +135,9 @@ const addTokensToRequestBody = ( if (includedFiles) { requestBody.included_files = includedFiles; } - + if (branch) { + requestBody.branch = branch; + } }; const createGithubHeaders = (githubToken: string): HeadersInit => { @@ -199,6 +202,7 @@ export default function RepoWikiPage() { : repoUrl?.includes('github.com') ? 'github' : searchParams.get('type') || 'github'; + const branch = searchParams.get('branch') || ''; // Import language context for translations const { messages } = useLanguage(); @@ -210,8 +214,9 @@ export default function RepoWikiPage() { type: repoType, token: token || null, localPath: localPath || null, - repoUrl: repoUrl || null - }), [owner, repo, repoType, localPath, repoUrl, token]); + repoUrl: repoUrl || null, + branch: branch || null, + }), [owner, repo, repoType, localPath, repoUrl, token, branch]); // State variables const [isLoading, setIsLoading] = useState(true); @@ -511,7 +516,7 @@ Remember: }; // Add tokens if available - addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles); + addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles, branch); // Use WebSocket for communication let content = ''; @@ -808,7 +813,7 @@ IMPORTANT: }; // Add tokens if available - addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles); + addTokensToRequestBody(requestBody, currentToken, effectiveRepoInfo.type, selectedProviderState, selectedModelState, isCustomSelectedModelState, customSelectedModelState, language, modelExcludedDirs, modelExcludedFiles, modelIncludedDirs, modelIncludedFiles, branch); // Use WebSocket for communication let responseText = ''; @@ -1211,22 +1216,31 @@ IMPORTANT: }; const githubApiBaseUrl = getGithubApiUrl(effectiveRepoInfo.repoUrl); - // First, try to get the default branch from the repository info - let defaultBranchLocal = null; - try { - const repoInfoResponse = await fetch(`${githubApiBaseUrl}/repos/${owner}/${repo}`, { - headers: createGithubHeaders(currentToken) - }); - - if (repoInfoResponse.ok) { - const repoData = await repoInfoResponse.json(); - defaultBranchLocal = repoData.default_branch; - console.log(`Found default branch: ${defaultBranchLocal}`); - // Store the default branch in state - setDefaultBranch(defaultBranchLocal || 'main'); + // First, try to get the default branch from effectiveRepoInfo or the repository info + let defaultBranchLocal = effectiveRepoInfo?.branch && `${effectiveRepoInfo.branch}`.trim() !== '' + ? `${effectiveRepoInfo.branch}`.trim() + : null; + + // If branch not provided, fetch repository info to determine default branch + if (!defaultBranchLocal) { + try { + const repoInfoResponse = await fetch(`${githubApiBaseUrl}/repos/${owner}/${repo}`, { + headers: createGithubHeaders(currentToken) + }); + + if (repoInfoResponse.ok) { + const repoData = await repoInfoResponse.json(); + defaultBranchLocal = repoData.default_branch; + console.log(`Found default branch: ${defaultBranchLocal}`); + // Store the default branch in state + setDefaultBranch(defaultBranchLocal || 'main'); + } + } catch (err) { + console.warn('Could not fetch repository info for default branch:', err); } - } catch (err) { - console.warn('Could not fetch repository info for default branch:', err); + } else { + // If we already have a branch from effectiveRepoInfo, store it + setDefaultBranch(defaultBranchLocal); } // Create list of branches to try, prioritizing the actual default branch @@ -2243,7 +2257,7 @@ IMPORTANT: onApply={confirmRefresh} showWikiType={true} showTokenInput={effectiveRepoInfo.type !== 'local' && !currentToken} // Show token input if not local and no current token - repositoryType={effectiveRepoInfo.type as 'github' | 'gitlab' | 'bitbucket'} + repositoryType={effectiveRepoInfo.type as 'github' | 'gitlab' | 'bitbucket' | 'azure'} authRequired={authRequired} authCode={authCode} setAuthCode={setAuthCode} diff --git a/src/app/page.tsx b/src/app/page.tsx index 9e05a2ef9..4c7cf4e84 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -76,6 +76,7 @@ export default function Home() { }; const [repositoryInput, setRepositoryInput] = useState('https://github.com/AsyncFuncAI/deepwiki-open'); + const [branch, setBranch] = useState(''); const REPO_CONFIG_CACHE_KEY = 'deepwikiRepoConfigCache'; @@ -98,6 +99,7 @@ export default function Home() { setExcludedFiles(config.excludedFiles || ''); setIncludedDirs(config.includedDirs || ''); setIncludedFiles(config.includedFiles || ''); + setBranch(config.branch || ''); } } } catch (error) { @@ -134,7 +136,7 @@ export default function Home() { const [excludedFiles, setExcludedFiles] = useState(''); const [includedDirs, setIncludedDirs] = useState(''); const [includedFiles, setIncludedFiles] = useState(''); - const [selectedPlatform, setSelectedPlatform] = useState<'github' | 'gitlab' | 'bitbucket'>('github'); + const [selectedPlatform, setSelectedPlatform] = useState<'github' | 'gitlab' | 'bitbucket' | 'azure'>('github'); const [accessToken, setAccessToken] = useState(''); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -212,6 +214,8 @@ export default function Home() { type = 'gitlab'; } else if (domain?.includes('bitbucket.org') || domain?.includes('bitbucket.')) { type = 'bitbucket'; + } else if (domain?.includes('azure.com') || domain?.includes('dev.azure.com')) { + type = 'azure'; } else { type = 'web'; // fallback for other git hosting services } @@ -221,6 +225,13 @@ export default function Home() { if (parts.length >= 2) { repo = parts[parts.length - 1] || ''; owner = parts[parts.length - 2] || ''; + if (type === 'azure') { + const userInfoMatch = input.match(/^(?:https?:\/\/)?([^@\/=\s]+)@/); + const candidateOwner = userInfoMatch?.[1] || parts[0] || ''; + owner = candidateOwner === '_git' ? (parts[0] || '') : candidateOwner; + } else { + owner = parts[parts.length - 2] || ''; + } } } // Unsupported URL formats @@ -322,6 +333,7 @@ export default function Home() { excludedFiles, includedDirs, includedFiles, + branch, }; existingConfigs[currentRepoUrl] = configToSave; localStorage.setItem(REPO_CONFIG_CACHE_KEY, JSON.stringify(existingConfigs)); @@ -376,6 +388,11 @@ export default function Home() { params.append('included_files', includedFiles); } + // Add branch parameter if provided + if (branch && branch.trim() !== '') { + params.append('branch', branch.trim()); + } + // Add language parameter params.append('language', selectedLanguage); @@ -445,6 +462,8 @@ export default function Home() { isOpen={isConfigModalOpen} onClose={() => setIsConfigModalOpen(false)} repositoryInput={repositoryInput} + branch={branch} + setBranch={setBranch} selectedLanguage={selectedLanguage} setSelectedLanguage={setSelectedLanguage} supportedLanguages={supportedLanguages} diff --git a/src/components/ConfigurationModal.tsx b/src/components/ConfigurationModal.tsx index 7a1dae697..60015ee5d 100644 --- a/src/components/ConfigurationModal.tsx +++ b/src/components/ConfigurationModal.tsx @@ -12,6 +12,10 @@ interface ConfigurationModalProps { // Repository input repositoryInput: string; + // Branch input + branch: string; + setBranch: (value: string) => void; + // Language selection selectedLanguage: string; setSelectedLanguage: (value: string) => void; @@ -32,8 +36,8 @@ interface ConfigurationModalProps { setCustomModel: (value: string) => void; // Platform selection - selectedPlatform: 'github' | 'gitlab' | 'bitbucket'; - setSelectedPlatform: (value: 'github' | 'gitlab' | 'bitbucket') => void; + selectedPlatform: 'github' | 'gitlab' | 'bitbucket' | 'azure'; + setSelectedPlatform: (value: 'github' | 'gitlab' | 'bitbucket' | 'azure') => void; // Access token accessToken: string; @@ -64,6 +68,8 @@ export default function ConfigurationModal({ isOpen, onClose, repositoryInput, + branch, + setBranch, selectedLanguage, setSelectedLanguage, supportedLanguages, @@ -135,6 +141,29 @@ export default function ConfigurationModal({ + {/* Branch input */} +
+ + setBranch(e.target.value)} + placeholder="e.g., main, master, develop" + className="input-japanese block w-full px-3 py-2 text-sm rounded-md bg-transparent text-[var(--foreground)] focus:outline-none focus:border-[var(--accent-primary)]" + /> +
+ + + + + {t.form?.branchHelp || "If left empty, the repository's default branch will be used."} + +
+
+ {/* Language selection */}
)} diff --git a/src/messages/en.json b/src/messages/en.json index a62ee892c..9573853b4 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -33,6 +33,8 @@ "configureWiki": "Configure Wiki", "repoPlaceholder": "owner/repo or GitHub/GitLab/Bitbucket URL", "wikiLanguage": "Wiki Language", + "branchLabel": "Branch (Optional)", + "branchHelp": "If left empty, the repository's default branch will be used.", "modelOptions": "Model Options", "modelProvider": "Model Provider", "modelSelection": "Model Selection", diff --git a/src/messages/es.json b/src/messages/es.json index cace0b542..416620e99 100644 --- a/src/messages/es.json +++ b/src/messages/es.json @@ -33,6 +33,8 @@ "configureWiki": "Configurar Wiki", "repoPlaceholder": "propietario/repositorio o URL de GitHub/GitLab/Bitbucket", "wikiLanguage": "Idioma del Wiki", + "branchLabel": "Rama (Opcional)", + "branchHelp": "Si no ingresas nada, se usará la rama por defecto del repositorio.", "modelOptions": "Opciones de Modelo", "modelProvider": "Proveedor de Modelo", "modelSelection": "Selección de Modelo", diff --git a/src/messages/fr.json b/src/messages/fr.json index 4194b31be..48bda430f 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -33,6 +33,8 @@ "configureWiki": "Configurer le Wiki", "repoPlaceholder": "propriétaire/dépôt ou URL GitHub/GitLab/Bitbucket", "wikiLanguage": "Langue du Wiki", + "branchLabel": "Branche (facultatif)", + "branchHelp": "Si ce champ est vide, la branche par défaut du dépôt sera utilisée.", "modelOptions": "Options du Modèle", "modelProvider": "Fournisseur du Modèle", "modelSelection": "Sélection du Modèle", diff --git a/src/messages/ja.json b/src/messages/ja.json index 8692635f9..38ca327fc 100644 --- a/src/messages/ja.json +++ b/src/messages/ja.json @@ -33,6 +33,8 @@ "configureWiki": "Wiki設定", "repoPlaceholder": "所有者/リポジトリまたはGitHub/GitLab/BitbucketのURL", "wikiLanguage": "Wiki言語", + "branchLabel": "ブランチ(任意)", + "branchHelp": "空の場合、リポジトリのデフォルトブランチが使用されます。", "modelOptions": "モデルオプション", "modelProvider": "モデルプロバイダー", "modelSelection": "モデル選択", diff --git a/src/messages/kr.json b/src/messages/kr.json index 68666f3d6..d164f5ce8 100644 --- a/src/messages/kr.json +++ b/src/messages/kr.json @@ -33,6 +33,8 @@ "configureWiki": "위키 구성", "repoPlaceholder": "owner/repo 또는 GitHub/GitLab/Bitbucket URL", "wikiLanguage": "위키 언어", + "branchLabel": "브랜치(선택 사항)", + "branchHelp": "비워두면 저장소의 기본 브랜치가 사용됩니다.", "modelOptions": "모델 옵션", "modelProvider": "모델 제공자", "modelSelection": "모델 선택", diff --git a/src/messages/pt-br.json b/src/messages/pt-br.json index 3bb05575b..683fa9b10 100644 --- a/src/messages/pt-br.json +++ b/src/messages/pt-br.json @@ -33,6 +33,8 @@ "configureWiki": "Configurar Wiki", "repoPlaceholder": "proprietário/repo ou URL do GitHub/GitLab/Bitbucket", "wikiLanguage": "Idioma da Wiki", + "branchLabel": "Branch (Opcional)", + "branchHelp": "Se deixar em branco, a branch padrão do repositório será usada.", "modelOptions": "Opções de Modelo", "modelProvider": "Provedor de Modelo", "modelSelection": "Seleção de Modelo", diff --git a/src/messages/ru.json b/src/messages/ru.json index 6a29752b7..a90539034 100644 --- a/src/messages/ru.json +++ b/src/messages/ru.json @@ -33,6 +33,8 @@ "configureWiki": "Настроить Wiki", "repoPlaceholder": "owner/repo или URL GitHub/GitLab/Bitbucket", "wikiLanguage": "Язык Wiki", + "branchLabel": "Ветка (необязательно)", + "branchHelp": "Если оставить пустым, будет использована ветка по умолчанию репозитория.", "modelOptions": "Настройки модели", "modelProvider": "Поставщик модели", "modelSelection": "Выбор модели", diff --git a/src/messages/vi.json b/src/messages/vi.json index 7ac1933cd..ad0a2d3f9 100644 --- a/src/messages/vi.json +++ b/src/messages/vi.json @@ -33,6 +33,8 @@ "configureWiki": "Cấu hình Wiki", "repoPlaceholder": "owner/repo hoặc URL GitHub/GitLab/Bitbucket", "wikiLanguage": "Ngôn ngữ Wiki", + "branchLabel": "Nhánh (Tùy chọn)", + "branchHelp": "Nếu để trống, nhánh mặc định của repository sẽ được sử dụng.", "modelOptions": "Tùy chọn mô hình", "modelProvider": "Nhà cung cấp mô hình", "modelSelection": "Lựa chọn mô hình", diff --git a/src/messages/zh-tw.json b/src/messages/zh-tw.json index 67a1bd2db..f9ff5b829 100644 --- a/src/messages/zh-tw.json +++ b/src/messages/zh-tw.json @@ -33,6 +33,8 @@ "configureWiki": "設定 Wiki", "repoPlaceholder": "擁有者/儲存庫或 GitHub/GitLab/Bitbucket URL", "wikiLanguage": "Wiki 語言", + "branchLabel": "分支(選填)", + "branchHelp": "若留空,將使用儲存庫的預設分支。", "modelOptions": "模型選項", "modelProvider": "模型提供商", "modelSelection": "模型選擇", diff --git a/src/messages/zh.json b/src/messages/zh.json index 10fc7b229..1c2bbad8b 100644 --- a/src/messages/zh.json +++ b/src/messages/zh.json @@ -33,6 +33,8 @@ "configureWiki": "配置Wiki", "repoPlaceholder": "所有者/仓库或GitHub/GitLab/Bitbucket URL", "wikiLanguage": "Wiki语言", + "branchLabel": "分支(可选)", + "branchHelp": "如果留空,将使用仓库的默认分支。", "modelOptions": "模型选项", "modelProvider": "模型提供商", "modelSelection": "模型选择", diff --git a/src/types/repoinfo.tsx b/src/types/repoinfo.tsx index ec4f87e6b..ef5e6de0f 100644 --- a/src/types/repoinfo.tsx +++ b/src/types/repoinfo.tsx @@ -5,6 +5,7 @@ export interface RepoInfo { token: string | null; localPath: string | null; repoUrl: string | null; + branch: string | null; } export default RepoInfo; \ No newline at end of file