fix: clone permissions for private repos (#190)

- reverted back to Flask development server for development
- improved / suppressed git logging
- fixed cloning for private repos by including PAT in clone job
- add 'private repo' badge that replaces repository stats for private repos
This commit is contained in:
Samuel Chau
2025-03-30 20:42:31 +10:30
committed by GitHub
parent 37585067a3
commit e251a4e317
7 changed files with 189 additions and 39 deletions

View File

@@ -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()"]
CMD ["python", "-m", "app.main"]

View File

@@ -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

View File

@@ -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:

View File

@@ -85,6 +85,11 @@ def setup_logging():
'level': 'INFO',
'handlers': ['console', 'file'],
'propagate': False
},
'git': {
'level': 'ERROR',
'handlers': ['console', 'file'],
'propagate': False
}
}
}

View File

@@ -37,6 +37,20 @@ const RepoStats = ({repoStats}) => {
if (!repoStats) {
return <Loader size={14} className='animate-spin' />;
}
// For private repositories or when stats can't be loaded
if (repoStats.isPrivate) {
return (
<div className='flex items-center space-x-1'>
<span className='text-xs px-2 py-0.5 rounded-md bg-gray-700/70 border border-gray-600 text-gray-300 flex items-center'>
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
</svg>
Private
</span>
</div>
);
}
return (
<>

View File

@@ -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);

View File

@@ -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
});
}
}
};