import net.http
import json
import os
import time
// Configuration
const local_base_dir = './repos' // Directory where repos will be cloned if -c is used
const license_keys = ['mit', 'gpl-3.0', 'apache-2.0', 'bsd-3-clause', 'lgpl-3.0', 'mpl-2.0', 'unlicense', 'isc', 'cc0-1.0']
const sync_poll_interval = 5 // Seconds between sync status checks
const sync_timeout = 300 // Max seconds to wait for sync completion
// Structs
struct License {
key string
}
struct ParentRepo {
full_name string
}
struct Repo {
name string
clone_url string
language string
license ?License
fork bool
parent ?ParentRepo
description ?string
updated_at string
}
// Fetch repositories from GitHub API with pagination
fn fetch_repos(username string, token string) ![]Repo {
mut repos := []Repo{}
mut url := 'https://api.github.com/users/${username}/repos?per_page=100'
for url != '' {
mut req := http.new_request(.get, url, '')
req.header.add(.authorization, 'Bearer ${token}')
req.header.add(.accept, 'application/vnd.github.v3+json')
resp := req.do()!
if resp.status_code != 200 {
return error('Failed to fetch repos: ${resp.status_code} ${resp.body}')
}
page_repos := json.decode([]Repo, resp.body)!
repos << page_repos
link_header := resp.header.get(.link) or { '' }
next_url := get_next_url(link_header) or { '' }
url = next_url
}
return repos
}
// Parse the Link header to get the next page URL
fn get_next_url(link_header string) ?string {
parts := link_header.split(',')
for part in parts {
if part.contains('rel="next"') {
start := part.index('<') or { continue }
end := part.index('>') or { continue }
if start < end {
return part[start+1..end]
}
}
}
return none
}
// Sync a forked repository with its upstream parent
fn sync_fork(repo Repo, username string, token string) ! {
if !repo.fork {
return
}
parent := repo.parent or { return }
parent_parts := parent.full_name.split('/')
if parent_parts.len != 2 {
return error('Invalid parent repository name')
}
branch := 'main' // Default branch; could fetch repo's default branch if needed
payload := json.encode({'branch': branch})
url := 'https://api.github.com/repos/${username}/${repo.name}/merge-upstream'
mut req := http.new_request(.post, url, payload)
req.header.add(.authorization, 'Bearer ${token}')
req.header.add(.accept, 'application/vnd.github.v3+json')
req.header.add(.content_type, 'application/json')
resp := req.do()!
if resp.status_code != 200 {
return error('Failed to sync fork ${repo.name}: ${resp.status_code} ${resp.body}')
}
}
// Check if sync is complete by polling updated_at
fn wait_for_sync(repo Repo, username string, token string, initial_updated_at string) ! {
url := 'https://api.github.com/repos/${username}/${repo.name}'
start_time := time.now()
for {
mut req := http.new_request(.get, url, '')
req.header.add(.authorization, 'Bearer ${token}')
req.header.add(.accept, 'application/vnd.github.v3+json')
resp := req.do()!
if resp.status_code != 200 {
return error('Failed to check sync status for ${repo.name}: ${resp.status_code} ${resp.body}')
}
repo_data := json.decode(Repo, resp.body)!
if repo_data.updated_at != initial_updated_at {
return
}
now := time.now()
if (now.unix() - start_time.unix()) > sync_timeout {
return error('Timeout waiting for sync of ${repo.name}')
}
time.sleep(sync_poll_interval * time.second)
}
}
fn main() {
// Validate command-line arguments
if os.args.len < 3 {
eprintln('Usage: v run github_repo_processor.v <github_username> <github_token> [-c|--clone] [-f filter_terms...]')
exit(1)
}
username := os.args[1]
token := os.args[2]
// Parse command-line switches
mut clone_flag := false
mut filter_terms := []string{}
mut i := 3
for i < os.args.len {
arg := os.args[i]
if arg == '-c' || arg == '--clone' {
clone_flag = true
i++
} else if arg == '-f' {
i++
if i < os.args.len {
unsafe { filter_terms = os.args[i..] } // Silence implicit clone warning
}
break
} else {
eprintln('Unknown option: ${arg}')
exit(1)
}
}
// Parse filter terms into license and language filters
mut included_licenses := []string{}
mut excluded_licenses := []string{}
mut included_languages := []string{}
mut excluded_languages := []string{}
for term in filter_terms {
if term.starts_with('!') {
stripped := term[1..].to_lower()
if stripped in license_keys {
excluded_licenses << stripped
} else {
excluded_languages << stripped
}
} else {
stripped := term.to_lower()
if stripped in license_keys {
included_licenses << stripped
} else {
included_languages << stripped
}
}
}
// Fetch all repositories
repos := fetch_repos(username, token) or {
eprintln('Error fetching repositories: ${err}')
exit(1)
}
println('Found ${repos.len} repositories')
// Process each repository
mut filtered_repos := []Repo{}
for repo in repos {
license_key := if l := repo.license { l.key.to_lower() } else { '' }
language := repo.language.to_lower()
// Apply filters
license_condition := (included_licenses.len == 0 || license_key in included_licenses) &&
!(license_key in excluded_licenses)
language_condition := (included_languages.len == 0 || language in included_languages) &&
!(language in excluded_languages)
if license_condition && language_condition {
filtered_repos << repo
}
}
if filtered_repos.len == 0 {
println('No repositories match the specified filters')
return
}
if clone_flag {
// Clone or update the filtered repositories
for repo in filtered_repos {
if repo.fork {
println('Syncing fork: ${repo.name}')
initial_updated_at := repo.updated_at
sync_fork(repo, username, token) or {
eprintln('Error syncing ${repo.name}: ${err}')
continue
}
wait_for_sync(repo, username, token, initial_updated_at) or {
eprintln('Error waiting for sync of ${repo.name}: ${err}')
continue
}
}
repo_dir := os.join_path(local_base_dir, repo.name)
if !os.exists(local_base_dir) {
os.mkdir_all(local_base_dir)!
}
if os.exists(repo_dir) {
println('Updating repository: ${repo.name}')
os.execute_or_panic('git -C "${repo_dir}" pull')
} else {
println('Cloning repository: ${repo.name}')
os.execute_or_panic('git clone ${repo.clone_url} "${repo_dir}"')
}
}
println('All selected repositories cloned or updated successfully')
} else {
// Print repository info in Markdown table
println('| Name | License | Language | Forked From | Description |')
println('|------|---------|----------|-------------|-------------|')
for repo in filtered_repos {
license_key := if l := repo.license { l.key.to_lower() } else { '' }
language := repo.language.to_lower()
license_str := if license_key == '' { 'No license' } else { license_key }
language_str := if language == '' { 'No language' } else { language }
forked_from := if repo.fork { if p := repo.parent { p.full_name } else { 'N/A' } } else { 'N/A' }
description := repo.description or { 'No description' }
// Debug output to inspect fork and parent data
if repo.fork {
if p := repo.parent {
eprintln('DEBUG: ${repo.name} is a fork with parent ${p.full_name}')
} else {
eprintln('DEBUG: ${repo.name} is a fork but parent is none')
}
} else {
eprintln('DEBUG: ${repo.name} is not a fork')
}
println('| ${repo.name} | ${license_str} | ${language_str} | ${forked_from} | ${description} |')
}
println('\nRepository information printed successfully')
}
}