diff --git a/backend/Dockerfile b/backend/Dockerfile
index 5deb4d3..6e6b528 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -4,4 +4,4 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Use gunicorn with 10-minute timeout
-CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "600", "app.main:create_app()"]
\ No newline at end of file
+CMD ["python", "-m", "app.main"]
\ No newline at end of file
diff --git a/backend/app/git/auth/authenticate.py b/backend/app/git/auth/authenticate.py
index c26484b..37bf28a 100644
--- a/backend/app/git/auth/authenticate.py
+++ b/backend/app/git/auth/authenticate.py
@@ -15,12 +15,24 @@ class GitHubAuth:
def get_authenticated_url(https_url):
"""
Convert an HTTPS URL to include authentication via PAT.
+ Ensures the token is not duplicated in the URL.
"""
token = os.getenv("PROFILARR_PAT")
if not token:
raise ValueError(
"PROFILARR_PAT is not set in environment variables")
-
+
+ # Check if the URL already contains authentication
+ if "@" in https_url:
+ # Already has some form of authentication, remove it to add our token
+ # This handles URLs that might have a token already
+ protocol_part, rest = https_url.split("://", 1)
+ if "@" in rest:
+ # Remove any existing authentication
+ _, server_part = rest.split("@", 1)
+ https_url = f"{protocol_part}://{server_part}"
+
+ # Now add our token
authenticated_url = https_url.replace("https://", f"https://{token}@")
return authenticated_url
diff --git a/backend/app/git/repo/clone.py b/backend/app/git/repo/clone.py
index 329488d..801f82a 100644
--- a/backend/app/git/repo/clone.py
+++ b/backend/app/git/repo/clone.py
@@ -6,6 +6,7 @@ import logging
import yaml
from git.exc import GitCommandError
import git
+from ..auth.authenticate import GitHubAuth
logger = logging.getLogger(__name__)
@@ -19,16 +20,35 @@ def clone_repository(repo_url, repo_path):
# Initial clone attempt
logger.info(f"Starting clone operation for {repo_url}")
try:
+ # First try without authentication (for public repos)
repo = git.Repo.clone_from(repo_url, temp_dir)
logger.info("Repository clone successful")
except GitCommandError as e:
- if "remote: Repository not found" in str(e):
+ error_str = str(e)
+ # If authentication error, try with token
+ if "could not read Username" in error_str or "Authentication failed" in error_str:
+ logger.info("Initial clone failed due to authentication. Trying with token...")
+ try:
+ # Verify token availability
+ if not GitHubAuth.verify_token():
+ logger.error("Private repository requires GitHub authentication. Please configure PAT.")
+ return False, "This appears to be a private repository. Please configure PROFILARR_PAT environment variable."
+
+ # Get authenticated URL for private repositories
+ authenticated_url = GitHubAuth.get_authenticated_url(repo_url)
+ repo = git.Repo.clone_from(authenticated_url, temp_dir)
+ logger.info("Repository clone with authentication successful")
+ except GitCommandError as auth_e:
+ logger.error(f"Clone with authentication failed: {str(auth_e)}")
+ return False, f"Failed to clone repository: {str(auth_e)}"
+ # If repository not found, create new one
+ elif "remote: Repository not found" in error_str:
logger.info("Creating new repository - remote not found")
repo = git.Repo.init(temp_dir)
repo.create_remote('origin', repo_url)
else:
- logger.error(f"Clone failed: {str(e)}")
- return False, f"Failed to clone repository: {str(e)}"
+ logger.error(f"Clone failed: {error_str}")
+ return False, f"Failed to clone repository: {error_str}"
# Check if repo is empty
try:
diff --git a/backend/app/init.py b/backend/app/init.py
index bbdec4a..84e0f2f 100644
--- a/backend/app/init.py
+++ b/backend/app/init.py
@@ -85,6 +85,11 @@ def setup_logging():
'level': 'INFO',
'handlers': ['console', 'file'],
'propagate': False
+ },
+ 'git': {
+ 'level': 'ERROR',
+ 'handlers': ['console', 'file'],
+ 'propagate': False
}
}
}
diff --git a/frontend/src/components/settings/git/repo/ActiveRepo.jsx b/frontend/src/components/settings/git/repo/ActiveRepo.jsx
index ca32c19..ed596ac 100644
--- a/frontend/src/components/settings/git/repo/ActiveRepo.jsx
+++ b/frontend/src/components/settings/git/repo/ActiveRepo.jsx
@@ -37,6 +37,20 @@ const RepoStats = ({repoStats}) => {
if (!repoStats) {
return ;
}
+
+ // For private repositories or when stats can't be loaded
+ if (repoStats.isPrivate) {
+ return (
+
+ );
+ }
return (
<>
diff --git a/frontend/src/components/settings/git/repo/LinkRepo.jsx b/frontend/src/components/settings/git/repo/LinkRepo.jsx
index d91dfe5..715972b 100644
--- a/frontend/src/components/settings/git/repo/LinkRepo.jsx
+++ b/frontend/src/components/settings/git/repo/LinkRepo.jsx
@@ -21,9 +21,56 @@ const LinkRepo = ({isOpen, onClose, onSubmit}) => {
);
onSubmit();
} catch (error) {
- Alert.error(
- 'An unexpected error occurred while linking the repository.'
- );
+ // Check for specific error cases
+ if (error.response) {
+ const { status, data } = error.response;
+
+ if (data && data.error) {
+ // Authentication errors for private repos
+ if (data.error.includes("could not read Username") ||
+ data.error.includes("Authentication failed") ||
+ data.error.includes("authentication") ||
+ data.error.includes("PROFILARR_PAT")) {
+ Alert.error(
+ 'Authentication failed. Please ensure you have configured a valid GitHub Personal Access Token in your .env file (PROFILARR_PAT).'
+ );
+ }
+ // Repository not found
+ else if (data.error.includes("not found") || status === 404) {
+ Alert.error(
+ 'Repository not found. Please check the URL and ensure you have access to this repository.'
+ );
+ }
+ // Permission issues (general 403)
+ else if (data.error.includes("permission") ||
+ data.error.includes("error: 403") ||
+ status === 403) {
+ Alert.error(
+ 'Permission denied. Your GitHub token may not have sufficient permissions to access this repository. Ensure it has "Contents: Read & write" permission.'
+ );
+ }
+ // Write access issues - specifically check for this common error
+ else if (data.error.includes("remote: Write access to repository not granted")) {
+ Alert.error(
+ 'Your GitHub token does not have write access to this repository. Please update your token with the "Contents: Read & write" permission.'
+ );
+ }
+ // Any other error - use a generic message rather than showing the raw error
+ else {
+ Alert.error(
+ 'Failed to link repository. Please check the URL and your GitHub token permissions.'
+ );
+ }
+ } else {
+ // HTTP error without specific message
+ Alert.error(`Failed to link repository (Error ${status})`);
+ }
+ } else {
+ // Network or other errors
+ Alert.error(
+ 'Failed to connect to the server. Please check your network connection.'
+ );
+ }
console.error('Error linking repository:', error);
} finally {
setLoading(false);
diff --git a/frontend/src/components/settings/git/repo/RepoContainer.jsx b/frontend/src/components/settings/git/repo/RepoContainer.jsx
index 9a6d07d..2e9ad28 100644
--- a/frontend/src/components/settings/git/repo/RepoContainer.jsx
+++ b/frontend/src/components/settings/git/repo/RepoContainer.jsx
@@ -84,42 +84,94 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus, status}) => {
}
try {
- const [avatarResponse, repoResponse] = await Promise.all([
- !isAvatarValid &&
- fetch(`https://api.github.com/users/${owner}`),
- !isStatsValid &&
- fetch(`https://api.github.com/repos/${owner}/${repo}`)
- ]);
-
- if (!isAvatarValid && avatarResponse.ok) {
- const userData = await avatarResponse.json();
- localStorage.setItem(avatarCacheKey, userData.avatar_url);
- localStorage.setItem(
- `${avatarCacheKey}-timestamp`,
- Date.now().toString()
- );
- setAvatarUrl(userData.avatar_url);
+ // Try to fetch the avatar if needed
+ if (!isAvatarValid) {
+ try {
+ const avatarResponse = await fetch(`https://api.github.com/users/${owner}`);
+
+ if (avatarResponse.ok) {
+ const userData = await avatarResponse.json();
+ localStorage.setItem(avatarCacheKey, userData.avatar_url);
+ localStorage.setItem(
+ `${avatarCacheKey}-timestamp`,
+ Date.now().toString()
+ );
+ setAvatarUrl(userData.avatar_url);
+ } else if (avatarResponse.status === 404) {
+ // User not found - use first letter avatar
+ setAvatarUrl(null);
+ }
+ } catch (avatarError) {
+ console.warn('Could not fetch avatar:', avatarError);
+ // Keep using cached avatar if available
+ if (cachedAvatar) setAvatarUrl(cachedAvatar);
+ }
}
-
- if (!isStatsValid && repoResponse.ok) {
- const repoData = await repoResponse.json();
- const stats = {
- stars: repoData.stargazers_count,
- forks: repoData.forks_count,
- issues: repoData.open_issues_count
- };
- localStorage.setItem(statsCacheKey, JSON.stringify(stats));
- localStorage.setItem(
- `${statsCacheKey}-timestamp`,
- Date.now().toString()
- );
- setRepoStats(stats);
+
+ // Try to fetch repo stats if needed
+ if (!isStatsValid) {
+ try {
+ const repoResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
+
+ if (repoResponse.ok) {
+ const repoData = await repoResponse.json();
+ const stats = {
+ stars: repoData.stargazers_count,
+ forks: repoData.forks_count,
+ issues: repoData.open_issues_count
+ };
+ localStorage.setItem(statsCacheKey, JSON.stringify(stats));
+ localStorage.setItem(
+ `${statsCacheKey}-timestamp`,
+ Date.now().toString()
+ );
+ setRepoStats(stats);
+ } else if (repoResponse.status === 404 || repoResponse.status === 403) {
+ // Repository not found or private - set empty stats
+ const privateRepoStats = {
+ stars: '-',
+ forks: '-',
+ issues: '-',
+ isPrivate: true
+ };
+ setRepoStats(privateRepoStats);
+ localStorage.setItem(statsCacheKey, JSON.stringify(privateRepoStats));
+ localStorage.setItem(
+ `${statsCacheKey}-timestamp`,
+ Date.now().toString()
+ );
+ }
+ } catch (statsError) {
+ console.warn('Could not fetch repo stats:', statsError);
+ // Use cached stats or create private repo placeholder
+ if (cachedStats) {
+ setRepoStats(JSON.parse(cachedStats));
+ } else {
+ const privateRepoStats = {
+ stars: '-',
+ forks: '-',
+ issues: '-',
+ isPrivate: true
+ };
+ setRepoStats(privateRepoStats);
+ }
+ }
}
} catch (error) {
- console.error('Error fetching GitHub data:', error);
+ console.error('Error in fetchAvatarAndStats:', error);
+ // Use cached values if available
if (cachedAvatar && !isAvatarValid) setAvatarUrl(cachedAvatar);
- if (cachedStats && !isStatsValid)
+ if (cachedStats && !isStatsValid) {
setRepoStats(JSON.parse(cachedStats));
+ } else {
+ // Provide default stats for private repos
+ setRepoStats({
+ stars: '-',
+ forks: '-',
+ issues: '-',
+ isPrivate: true
+ });
+ }
}
};