Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Python CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests gitpython

- name: Run tests
run: |
python -m unittest discover tests
114 changes: 114 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
*.pyo
*.pyd

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyderworkspace

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

# GitHub Starred Repository Cloner

![Python CI](.github/workflows/ci.yml/badge.svg)

This Python script automates the process of cloning all repositories starred by a GitHub user. Simply provide your GitHub username and Personal Access Token (PAT), and it will fetch and clone the repositories into a local directory.

## Features
Expand Down Expand Up @@ -28,7 +30,7 @@ pip install -r requirements.txt
```

### 3. Configure Your Credentials
Edit `github-clone.py` and set your GitHub username and PAT:
Edit `github_clone.py` and set your GitHub username and PAT:
```python
username = 'Enter Your Username'
token = 'Enter Your PAT'
Expand All @@ -37,7 +39,7 @@ token = 'Enter Your PAT'
### 4. Run the Script
Execute the script to clone starred repositories:
```bash
python github-clone.py
python github_clone.py
```

## Troubleshooting
Expand Down
41 changes: 0 additions & 41 deletions github-clone.py

This file was deleted.

57 changes: 57 additions & 0 deletions github_clone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import requests
import git

def get_starred_repos(username, token):
"""
Fetches the list of starred repositories for a given user.
"""
url = f'https://api.github.com/users/{username}/starred'
response = requests.get(url, auth=(username, token))
response.raise_for_status() # Raise an exception for bad status codes
return response.json()

def clone_repo(repo_info, clone_dir):
"""
Clones a single repository into the specified directory.
"""
repo_name = repo_info['name']
repo_url = repo_info['clone_url']
repo_dir = os.path.join(clone_dir, repo_name)

if not os.path.exists(repo_dir):
print(f'Cloning {repo_name}...')
git.Repo.clone_from(repo_url, repo_dir)
print(f'Finished cloning {repo_name}')
else:
print(f'{repo_name} already exists, skipping...')

def main():
"""
Main function to clone starred GitHub repositories.
"""
# Your GitHub username
username = 'Enter Your Username'
# Example username = 'manupawickramasinghe'

# Your GitHub personal access token
token = 'Enter Your PAT'
# Example token = '123123123133'

# Directory to clone repos into
clone_dir = 'starred_repos'

# Create the directory if it doesn't exist
if not os.path.exists(clone_dir):
os.makedirs(clone_dir)

try:
repos = get_starred_repos(username, token)
for repo in repos:
clone_repo(repo, clone_dir)
print('All repositories have been cloned.')
except requests.exceptions.RequestException as e:
print(f"Error fetching repositories: {e}")

if __name__ == "__main__":
main()
28 changes: 28 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Unit Tests

This directory contains the unit tests for the GitHub Starred Repository Cloner.

## Framework

The tests are written using Python's built-in `unittest` framework. The `unittest.mock` library is used to simulate external dependencies like API calls and file system operations.

## Running the Tests

To run the tests, navigate to the root directory of the project and run the following command:

```bash
python -m unittest discover tests
```

This will automatically discover and run all the tests in this directory.

## Test Coverage

The tests cover the following functionality:

- **`get_starred_repos`**:
- Verifies that the function correctly parses a successful API response.
- Ensures that the function handles API errors gracefully.
- **`clone_repo`**:
- Checks that a new repository is cloned if it doesn't already exist.
- Confirms that an existing repository is skipped.
Empty file added tests/__init__.py
Empty file.
67 changes: 67 additions & 0 deletions tests/test_github_clone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import unittest
from unittest.mock import patch, MagicMock
import os
import sys

# Add the root directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from github_clone import get_starred_repos, clone_repo

class TestGitHubClone(unittest.TestCase):

@patch('github_clone.requests.get')
def test_get_starred_repos_success(self, mock_get):
# Mock the API response
mock_response = MagicMock()
mock_response.json.return_value = [{'name': 'repo1'}, {'name': 'repo2'}]
mock_response.raise_for_status = MagicMock()
mock_get.return_value = mock_response

repos = get_starred_repos('testuser', 'testtoken')
self.assertEqual(len(repos), 2)
self.assertEqual(repos[0]['name'], 'repo1')
mock_get.assert_called_with('https://api.github.com/users/testuser/starred', auth=('testuser', 'testtoken'))

@patch('github_clone.requests.get')
def test_get_starred_repos_failure(self, mock_get):
# Mock a failed API response
mock_response = MagicMock()
mock_response.raise_for_status.side_effect = Exception("API Error")
mock_get.return_value = mock_response

with self.assertRaises(Exception):
get_starred_repos('testuser', 'testtoken')

@patch('github_clone.git.Repo.clone_from')
@patch('github_clone.os.path.exists')
def test_clone_repo_new(self, mock_exists, mock_clone_from):
# Mock that the repo does not exist
mock_exists.return_value = False

repo_info = {'name': 'new_repo', 'clone_url': 'http://example.com/new_repo.git'}
clone_dir = 'test_dir'

clone_repo(repo_info, clone_dir)

repo_dir = os.path.join(clone_dir, repo_info['name'])
mock_exists.assert_called_with(repo_dir)
mock_clone_from.assert_called_with(repo_info['clone_url'], repo_dir)

@patch('github_clone.git.Repo.clone_from')
@patch('github_clone.os.path.exists')
def test_clone_repo_exists(self, mock_exists, mock_clone_from):
# Mock that the repo already exists
mock_exists.return_value = True

repo_info = {'name': 'existing_repo', 'clone_url': 'http://example.com/existing_repo.git'}
clone_dir = 'test_dir'

clone_repo(repo_info, clone_dir)

repo_dir = os.path.join(clone_dir, repo_info['name'])
mock_exists.assert_called_with(repo_dir)
mock_clone_from.assert_not_called()

if __name__ == '__main__':
unittest.main()