package main import ( "bytes" "encoding/json" "flag" "fmt" "log" "net/http" "os" "strings" "github.com/joho/godotenv" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/transport/ssh" ) 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"` } 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") fmt.Println("\nUsage:") fmt.Println(" fj [arguments]") fmt.Println("\nAvailable Commands:") fmt.Println(" list List all accessible repositories") fmt.Println(" clone Clone a repository to the specified directory") fmt.Println(" create Create a new repository") fmt.Println(" delete 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") } 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) } func buildApiUrlCreateRepo(host string, token string) string { return buildApiUrl(host, token, API_CREATE_REPO) } 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 } } return nil } func getRepos(host string, token string) ([]ForgejoRepository, error) { repos_json, err := http.Get(buildApiUrlUserRepos(host, token)) if err != nil { return nil, err } var repos []ForgejoRepository err = json.NewDecoder(repos_json.Body).Decode(&repos) if err != nil { return nil, err } return repos, nil } func listRepos(host string, token string) { repos, err := getRepos(host, token) if err != nil { log.Fatalln(err) return } for _, repo := range repos { log.Println(repo.Name + " " + repo.CloneURL) } } 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 ") 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 ") 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 ") 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) } }