2025-01-14 16:20:53 +01:00
package main
import (
2025-02-05 08:48:22 +01:00
"bytes"
2025-01-14 16:20:53 +01:00
"encoding/json"
2025-02-05 08:48:22 +01:00
"flag"
"fmt"
2025-01-14 16:20:53 +01:00
"log"
2025-02-05 08:48:22 +01:00
"net/http"
2025-01-14 16:20:53 +01:00
"os"
2025-02-05 08:48:22 +01:00
"strings"
2025-01-14 16:20:53 +01:00
2025-02-05 08:48:22 +01:00
"github.com/joho/godotenv"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
2025-01-14 16:20:53 +01:00
)
2025-02-05 11:05:14 +01:00
var Version = "dev"
2025-01-14 16:20:53 +01:00
type ForgejoRepository struct {
ID int ` json:"id" `
Name string ` json:"name" `
FullName string ` json:"full_name" `
HTMLURL string ` json:"html_url" `
SSHURL string ` json:"ssh_url" `
CloneURL string ` json:"clone_url" `
}
2025-02-05 08:48:22 +01:00
const (
API_URL = "/api/v1"
API_USER_REPOS = "/user/repos"
API_CREATE_REPO = "/user/repos"
API_DELETE_REPO = "/repos/%s/%s"
)
func showHelp ( ) {
fmt . Println ( "fj - A Forgejo CLI" )
2025-02-05 11:05:14 +01:00
fmt . Println ( "Version:" , Version )
2025-02-05 08:48:22 +01:00
fmt . Println ( "\nUsage:" )
fmt . Println ( " fj <command> [arguments]" )
fmt . Println ( "\nAvailable Commands:" )
fmt . Println ( " list List all accessible repositories" )
fmt . Println ( " clone <repo> <dir> Clone a repository to the specified directory" )
fmt . Println ( " create <repo> Create a new repository" )
fmt . Println ( " delete <repo> Delete an existing repository" )
fmt . Println ( " help Show this help message" )
fmt . Println ( "\nEnvironment Variables:" )
fmt . Println ( " FJ_HOST Forgejo instance URL" )
fmt . Println ( " FJ_USERNAME Username to use for repository operations" )
fmt . Println ( " FJ_API_TOKEN Forgejo API token" )
fmt . Println ( " FJ_SSH_KEY_PATH Path to SSH key for git operations" )
fmt . Println ( " FJ_SSH_USER Username to use for SSH git operations" )
}
2025-01-14 16:20:53 +01:00
func buildApiUrl ( host string , token string , api string ) string {
return host + API_URL + api + "?token=" + token + "&limit=1000"
}
func buildApiUrlUserRepos ( host string , token string ) string {
return buildApiUrl ( host , token , API_USER_REPOS )
}
2025-02-05 08:48:22 +01:00
func buildApiUrlCreateRepo ( host string , token string ) string {
return buildApiUrl ( host , token , API_CREATE_REPO )
}
2025-01-14 16:20:53 +01:00
2025-02-05 08:48:22 +01:00
func buildApiUrlDeleteRepo ( host string , token string , username string , reponame string ) string {
return buildApiUrl ( host , token , fmt . Sprintf ( API_DELETE_REPO , username , reponame ) )
}
func findRepo ( repos [ ] ForgejoRepository , name string ) * ForgejoRepository {
for _ , repo := range repos {
if repo . Name == name {
return & repo
}
2025-01-14 16:20:53 +01:00
}
2025-02-05 08:48:22 +01:00
return nil
}
2025-01-14 16:20:53 +01:00
2025-02-05 08:48:22 +01:00
func getRepos ( host string , token string ) ( [ ] ForgejoRepository , error ) {
repos_json , err := http . Get ( buildApiUrlUserRepos ( host , token ) )
2025-01-14 16:20:53 +01:00
if err != nil {
2025-02-05 08:48:22 +01:00
return nil , err
2025-01-14 16:20:53 +01:00
}
var repos [ ] ForgejoRepository
err = json . NewDecoder ( repos_json . Body ) . Decode ( & repos )
2025-02-05 08:48:22 +01:00
if err != nil {
return nil , err
}
return repos , nil
}
func listRepos ( host string , token string ) {
repos , err := getRepos ( host , token )
2025-01-14 16:20:53 +01:00
if err != nil {
log . Fatalln ( err )
return
}
for _ , repo := range repos {
log . Println ( repo . Name + " " + repo . CloneURL )
}
}
2025-02-05 08:48:22 +01:00
func cloneRepo ( host string , token string , sshUser string , keyPath string , repoName string , directory string ) {
repos , err := getRepos ( host , token )
if err != nil {
log . Fatalln ( err )
return
}
repo := findRepo ( repos , repoName )
if repo == nil {
log . Fatalf ( "Repository '%s' not found\n" , repoName )
return
}
publicKeys , err := ssh . NewPublicKeysFromFile ( sshUser , keyPath , "" )
if err != nil {
log . Fatalf ( "Failed to load SSH key: %v\n" , err )
return
}
_ , err = git . PlainClone ( directory , false , & git . CloneOptions {
URL : repo . SSHURL ,
Progress : os . Stdout ,
Auth : publicKeys ,
} )
if err != nil {
log . Fatalf ( "Failed to clone repository: %v\n" , err )
return
}
fmt . Printf ( "Successfully cloned %s to %s\n" , repoName , directory )
}
func createRepo ( host string , token string , repoName string ) error {
// Confirm with user
fmt . Printf ( "Are you sure you want to create repository '%s'? (y/N): " , repoName )
var response string
fmt . Scanln ( & response )
if strings . ToLower ( response ) != "y" {
return fmt . Errorf ( "repository creation cancelled" )
}
// Prepare request body
reqBody := map [ string ] interface { } {
"name" : repoName ,
"private" : true ,
}
jsonBody , err := json . Marshal ( reqBody )
if err != nil {
return err
}
// Create POST request
url := buildApiUrlCreateRepo ( host , token )
req , err := http . NewRequest ( "POST" , url , bytes . NewBuffer ( jsonBody ) )
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/json" )
// Send request
client := & http . Client { }
resp , err := client . Do ( req )
if err != nil {
return err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusCreated {
return fmt . Errorf ( "failed to create repository. Status: %s" , resp . Status )
}
fmt . Printf ( "Repository '%s' created successfully\n" , repoName )
repos , err := getRepos ( host , token )
if err != nil {
log . Fatalln ( err )
return err
}
repo := findRepo ( repos , repoName )
if repo == nil {
log . Fatalf ( "Repository '%s' not found\n" , repoName )
return err
}
// print the repo ssh clone url
fmt . Printf ( "SSH clone URL: %s\n" , repo . SSHURL )
return nil
}
func deleteRepo ( host string , token string , username string , repoName string ) error {
// First confirmation
fmt . Printf ( "Are you sure you want to delete repository '%s'? (y/N): " , repoName )
var response string
fmt . Scanln ( & response )
if strings . ToLower ( response ) != "y" {
return fmt . Errorf ( "repository deletion cancelled" )
}
// Second confirmation with warning
color := "\033[31m" // Red color
reset := "\033[0m" // Reset color
fmt . Printf ( "%sWARNING: This will permanently delete all data in '%s'. This action cannot be undone!\nType the repository name to confirm: %s" , color , repoName , reset )
var confirmation string
fmt . Scanln ( & confirmation )
if confirmation != repoName {
return fmt . Errorf ( "repository deletion cancelled - name mismatch" )
}
// Create DELETE request
url := buildApiUrlDeleteRepo ( host , token , username , repoName )
req , err := http . NewRequest ( "DELETE" , url , nil )
if err != nil {
return err
}
// Send request
client := & http . Client { }
resp , err := client . Do ( req )
if err != nil {
return err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusNoContent {
return fmt . Errorf ( "failed to delete repository. Status: %s" , resp . Status )
}
fmt . Printf ( "Repository '%s' deleted successfully\n" , repoName )
return nil
}
func main ( ) {
// Get the environment variables for Forgejo URL and api token
FJ_HOST := os . Getenv ( "FJ_HOST" )
FJ_USERNAME := os . Getenv ( "FJ_USERNAME" )
FJ_API_TOKEN := os . Getenv ( "FJ_API_TOKEN" )
FJ_SSH_KEY_PATH := os . Getenv ( "FJ_SSH_KEY_PATH" )
FJ_SSH_USER := os . Getenv ( "FJ_SSH_USER" )
if FJ_HOST == "" || FJ_USERNAME == "" || FJ_API_TOKEN == "" || FJ_SSH_KEY_PATH == "" || FJ_SSH_USER == "" {
if err := godotenv . Load ( ) ; err != nil {
log . Fatalln ( "Please configure the FJ_HOST, FJ_API_TOKEN, FJ_SSH_KEY_PATH and FJ_SSH_USER environment variables to use fj." )
return
}
FJ_HOST = os . Getenv ( "FJ_HOST" )
FJ_USERNAME = os . Getenv ( "FJ_USERNAME" )
FJ_API_TOKEN = os . Getenv ( "FJ_API_TOKEN" )
FJ_SSH_KEY_PATH = os . Getenv ( "FJ_SSH_KEY_PATH" )
FJ_SSH_USER = os . Getenv ( "FJ_SSH_USER" )
}
flag . Parse ( )
command := ""
if flag . NArg ( ) > 0 {
command = flag . Arg ( 0 )
}
switch command {
case "list" :
listRepos ( FJ_HOST , FJ_API_TOKEN )
case "clone" :
if flag . NArg ( ) != 3 {
log . Fatalln ( "Usage: fj clone <repository-name> <directory>" )
return
}
repoName := flag . Arg ( 1 )
directory := flag . Arg ( 2 )
cloneRepo ( FJ_HOST , FJ_API_TOKEN , FJ_SSH_USER , FJ_SSH_KEY_PATH , repoName , directory )
case "create" :
if flag . NArg ( ) != 2 {
log . Fatalln ( "Usage: fj create <repository-name>" )
return
}
repoName := flag . Arg ( 1 )
if err := createRepo ( FJ_HOST , FJ_API_TOKEN , repoName ) ; err != nil {
log . Fatalln ( err )
}
case "delete" :
if flag . NArg ( ) != 2 {
log . Fatalln ( "Usage: fj delete <repository-name>" )
return
}
repoName := flag . Arg ( 1 )
if FJ_USERNAME == "" {
log . Fatalln ( "Please set FJ_USERNAME environment variable" )
return
}
if err := deleteRepo ( FJ_HOST , FJ_API_TOKEN , FJ_USERNAME , repoName ) ; err != nil {
log . Fatalln ( err )
}
case "help" :
showHelp ( )
default :
showHelp ( )
os . Exit ( 1 )
}
}