mirror of
https://github.com/tiennm99/MTTools.git
synced 2026-08-14 09:23:21 +00:00
feat: add gitea-leave-orgs script and init Go module
Add cmd/ structure for multiple standalone scripts. First script leaves all joined Gitea orgs except a configurable keep-list.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
GITEA_URL=https://gitea.example.com
|
||||
GITEA_TOKEN=your-api-token-here
|
||||
GITEA_KEEP_ORGS=org1,org2
|
||||
+34
-1
@@ -1,2 +1,35 @@
|
||||
# go-util
|
||||
Some useful (or useless) scripts written in Go
|
||||
|
||||
Some useful (or useless) scripts written in Go.
|
||||
|
||||
## Structure
|
||||
|
||||
Each script lives in its own directory under `cmd/`, so multiple `main` packages coexist without conflicts.
|
||||
|
||||
```
|
||||
cmd/
|
||||
gitea-leave-orgs/ # Leave all Gitea orgs except a keep-list
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run any script
|
||||
go run ./cmd/<script-name>
|
||||
|
||||
# Example: leave Gitea orgs
|
||||
export GITEA_URL=https://gitea.example.com
|
||||
export GITEA_TOKEN=your-token
|
||||
export GITEA_KEEP_ORGS=org1,org2
|
||||
go run ./cmd/gitea-leave-orgs
|
||||
```
|
||||
|
||||
## Adding a new script
|
||||
|
||||
Create a new directory under `cmd/` with its own `main.go`:
|
||||
|
||||
```bash
|
||||
mkdir cmd/my-new-script
|
||||
# write cmd/my-new-script/main.go with `package main`
|
||||
go run ./cmd/my-new-script
|
||||
```
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// org represents a Gitea organization.
|
||||
type org struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
baseURL := os.Getenv("GITEA_URL")
|
||||
token := os.Getenv("GITEA_TOKEN")
|
||||
keepList := os.Getenv("GITEA_KEEP_ORGS") // comma-separated org names to keep
|
||||
|
||||
if baseURL == "" || token == "" {
|
||||
log.Fatal("GITEA_URL and GITEA_TOKEN env vars are required")
|
||||
}
|
||||
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
|
||||
keep := make(map[string]bool)
|
||||
for _, name := range strings.Split(keepList, ",") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
keep[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
orgs, err := listOrgs(baseURL, token)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to list orgs: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d orgs, keeping: %v\n", len(orgs), mapsKeys(keep))
|
||||
|
||||
for _, o := range orgs {
|
||||
if keep[o.Username] {
|
||||
fmt.Printf(" KEEP %s\n", o.Username)
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" LEAVE %s ... ", o.Username)
|
||||
if err := leaveOrg(baseURL, token, o.Username); err != nil {
|
||||
fmt.Printf("FAILED: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listOrgs fetches all organizations the authenticated user belongs to.
|
||||
func listOrgs(baseURL, token string) ([]org, error) {
|
||||
var allOrgs []org
|
||||
page := 1
|
||||
|
||||
for {
|
||||
url := fmt.Sprintf("%s/api/v1/user/orgs?page=%d&limit=50", baseURL, page)
|
||||
body, err := doRequest(http.MethodGet, url, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orgs []org
|
||||
if err := json.Unmarshal(body, &orgs); err != nil {
|
||||
return nil, fmt.Errorf("decode orgs: %w", err)
|
||||
}
|
||||
if len(orgs) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
allOrgs = append(allOrgs, orgs...)
|
||||
page++
|
||||
}
|
||||
|
||||
return allOrgs, nil
|
||||
}
|
||||
|
||||
// leaveOrg removes the authenticated user from the given organization.
|
||||
func leaveOrg(baseURL, token, orgName string) error {
|
||||
url := fmt.Sprintf("%s/api/v1/orgs/%s/members/me", baseURL, orgName)
|
||||
|
||||
// First try the /members/me endpoint
|
||||
_, err := doRequest(http.MethodDelete, url, token)
|
||||
if err != nil {
|
||||
// Fallback: get username and use explicit member removal
|
||||
username, userErr := getUsername(baseURL, token)
|
||||
if userErr != nil {
|
||||
return fmt.Errorf("leave org: %w (also failed to get username: %v)", err, userErr)
|
||||
}
|
||||
url = fmt.Sprintf("%s/api/v1/orgs/%s/members/%s", baseURL, orgName, username)
|
||||
_, err = doRequest(http.MethodDelete, url, token)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// getUsername returns the authenticated user's username.
|
||||
func getUsername(baseURL, token string) (string, error) {
|
||||
body, err := doRequest(http.MethodGet, baseURL+"/api/v1/user", token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var user struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &user); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return user.Login, nil
|
||||
}
|
||||
|
||||
// doRequest executes an HTTP request with token auth and returns the response body.
|
||||
func doRequest(method, url, token string) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func mapsKeys(m map[string]bool) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/tiennm99/go-util
|
||||
|
||||
go 1.26.1
|
||||
Reference in New Issue
Block a user