Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Adding Organisation support #3

Merged
merged 4 commits into from
Sep 29, 2022
Merged
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
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
![Alt text](resources/gh/header.png?raw=true "Submoduler")

An app that iterates a list of repositories and updates each of their submodules to the latest commit.

# TODOs

- [x] Run Process for 1 or more repositories using HTTPS and PAT.
- [ ] Run process for entire organization.

## Build the Docker Image

Expand Down Expand Up @@ -52,4 +47,17 @@ interval: 3
- **init**: if not initialised, init the submodules.
- **force_reset**: remove any local change and force reset on the submodules.
- **recursive**: update the children submodules.
- organization: this key can contain a organization name and configuration for submoduler.
e.g.
```yaml
organization:
test-org-name:
to_latest_revision: true
init: true
force_reset: true
recursive: true
interval: 3
```
When specifying a Organization, all the repos living under it will be pulled and monitored/updated.
Right now, only one Submoduler Configuration is supported for all the Org repos, and only 1 repo is supported.
- **interval**: this defines how often the check is performed on your repository submodules in seconds.
65 changes: 64 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ PyYAML = "^6.0"
loguru = "^0.6.0"
GitPython = "^3.1.27"
dacite = "^1.6.0"
requests = "^2.28.1"

[tool.poetry.dev-dependencies]

Expand Down
2 changes: 1 addition & 1 deletion submoduler-example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ repos:
init: true
force_reset: true
recursive: true
interval: 3
interval: 3
7 changes: 7 additions & 0 deletions submoduler-organization-example-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
organization:
test-org-name:
to_latest_revision: true
init: true
force_reset: true
recursive: true
interval: 3
2 changes: 1 addition & 1 deletion submoduler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@click.command
@click.option('--config_path',
help="Path to the configuration file. This supports relative path, e.g.: ../submoduler.yaml.",
help="Path to the configuration file. This supports relative path, e.g.: ../submoduler-example-config.yaml.",
default="/opt/submoduler.yaml")
@click.option('--user',
help="Username.",
Expand Down
75 changes: 55 additions & 20 deletions submoduler/submoduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import threading
import subprocess
import traceback
import requests

import git

Expand All @@ -16,6 +17,7 @@
class Submoduler:
"""Core class with the only responsibility to parse repositories and perform the Git Operations."""
_CACHE_DIR = os.path.join(os.path.expanduser("~/"), "submoduler", "repos")
_GH_API_URL = " https://api.github.com"

def __init__(self, configuration: dict, user: str, pat: str, email: str):
"""Init.
Expand All @@ -35,12 +37,19 @@ def __init__(self, configuration: dict, user: str, pat: str, email: str):
if user is None:
raise Exception("No Username defined.")

self._user = user
self._pat = pat
self._set_credentials(user, email, pat)

self._interval = configuration.get("interval")
logger.info(f"Interval set to {self._interval}")

self._repos_configs = configuration.get("repos")
self._repos_configs = configuration.get("repos", {})
self._organization = configuration.get("organization", {})
if len(self._organization) > 1:
error = "Only one Organization is supported at this time. Picking the first and discarding the others."
logger.error(error)
raise Exception(error)

self._repos: list[RepoMeta] = []
os.makedirs(self._CACHE_DIR, exist_ok=True)
Expand Down Expand Up @@ -88,28 +97,54 @@ def _make_repo(self, repo_path: str, repo_meta: dict):

self._repos.append(repo_meta_class)

def _get_org_repos(self, org_name: str) -> list[dict]:
"""Given an organization name, return all its repositories.

Args:
org_name: organization name.

Returns:
list: list of dict.
"""
return requests.get(f"{self._GH_API_URL}/orgs/{org_name}/repos", auth=(self._user, self._pat)).json()

def _clone_repo(self, repo_url, repo_clone_path):
if repo_url.lower().startswith(("https")):
try:
Repo.clone_from(repo_url, repo_clone_path)
except git.GitCommandError as git_error:
if "already exists" in git_error.stderr:
pass
else:
logger.error(traceback.format_exc())
raise Exception(git_error.stderr)

else:
# Not a URL, unknown format.
logger.warning(f"Couldn't recognize a format for url: {repo_url}")

def _process_repo(self, repo_url: str, repo_clone_path: str, repo_meta: dict):
"""Clones a Repo and creates a RepoMeta object stored in the class.

Args:
repo_url: https url of the repository to clone.
repo_clone_path: local path where the repository should be cloned.
repo_meta: dictionary of metadata for the RepoMeta object.
"""
self._clone_repo(repo_url, repo_clone_path)
self._make_repo(repo_clone_path, repo_meta)

def _parse_repos(self):
"""Parse repositories listed in the configuration and populates self._repos with RepoMeta objects."""
for repo_name, repo_meta in self._repos_configs.items():
repo_url = repo_meta.get("url")

if repo_url.lower().startswith(("https")):
repo_clone_path = os.path.join(self._CACHE_DIR, repo_name)
try:
#repo_url = repo_url.replace("https://", f"https://{self._pat}@")
Repo.clone_from(repo_url, repo_clone_path)
except git.GitCommandError as git_error:
if "already exists" in git_error.stderr:
pass
else:
logger.error(traceback.format_exc())
raise Exception(git_error.stderr)

self._make_repo(repo_clone_path, repo_meta)

else:
# Not a URL, unknown format.
logger.warning(f"Couldn't recognize a format for url: {repo_url}")
repo_clone_path = os.path.join(self._CACHE_DIR, repo_name)
self._process_repo(repo_meta.get("url"), repo_clone_path, repo_meta)

for org_name, org_meta in self._organization.items():
repos = self._get_org_repos(org_name)
for repo_dict in repos:
repo_clone_path = os.path.join(self._CACHE_DIR, org_name, repo_dict.get("name"))
self._process_repo(repo_dict.get("html_url"), repo_clone_path, org_meta)

def _start(self):
"""Starts a thread for each repository to update its submodules every 'interval' as specified in the configuration."""
Expand Down