mirror of
https://github.com/go-i2p/gitlab-to-gitea.git
synced 2025-06-16 13:54:47 -04:00
add org fixer
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,5 +27,6 @@ go.work.sum
|
||||
/unmigrate
|
||||
/forkfix
|
||||
/migration_state.json
|
||||
/bin
|
||||
log.log
|
||||
err.log
|
410
cmd/orgfix/main.go
Normal file
410
cmd/orgfix/main.go
Normal file
@ -0,0 +1,410 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Repository represents a Gitea repository record
|
||||
type Repository struct {
|
||||
ID int64
|
||||
Name string
|
||||
OwnerID int64
|
||||
OwnerName string
|
||||
IsFork bool
|
||||
ForkID sql.NullInt64
|
||||
}
|
||||
|
||||
// User represents a Gitea user or organization
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
Type int // 1 for individual, 2 for organization
|
||||
}
|
||||
|
||||
const (
|
||||
UserTypeIndividual = 0
|
||||
UserTypeOrganization = 1
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command line flags
|
||||
dbPath := flag.String("db", "", "Path to gitea.db (required)")
|
||||
orgName := flag.String("org", "", "Name of the organization to match against (required)")
|
||||
dryRun := flag.Bool("dry-run", false, "Perform a dry run without making changes")
|
||||
backup := flag.Bool("backup", true, "Create a backup of the database before making changes")
|
||||
verbose := flag.Bool("verbose", false, "Show detailed output")
|
||||
help := flag.Bool("help", false, "Show help")
|
||||
listOrgs := flag.Bool("list-orgs", false, "List all organizations in the database")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *listOrgs {
|
||||
if *dbPath == "" {
|
||||
log.Fatal("Database path (-db) is required")
|
||||
}
|
||||
db, err := sql.Open("sqlite3", *dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Verify database connection
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("Failed to connect to database (possibly locked or invalid): %v", err)
|
||||
}
|
||||
|
||||
listOrganizations(db)
|
||||
return
|
||||
}
|
||||
|
||||
// Display help if requested or if no arguments provided
|
||||
if *help || flag.NFlag() == 0 {
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required arguments
|
||||
if *dbPath == "" {
|
||||
log.Fatal("Database path (-db) is required")
|
||||
}
|
||||
|
||||
if *orgName == "" {
|
||||
log.Fatal("Organization name (-org) is required")
|
||||
}
|
||||
|
||||
// Create backup if requested and not in dry run mode
|
||||
if *backup && !*dryRun {
|
||||
backupFile := backupDatabase(*dbPath)
|
||||
fmt.Printf("Created backup at: %s\n", backupFile)
|
||||
}
|
||||
|
||||
// Open database connection
|
||||
db, err := sql.Open("sqlite3", *dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Verify the database connection
|
||||
if err := db.Ping(); err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Get organization ID
|
||||
orgID, err := getOrganizationID(db, *orgName)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to find organization '%s': %v", *orgName, err)
|
||||
}
|
||||
|
||||
// Get repositories owned by the organization
|
||||
orgRepos, err := getOrganizationRepos(db, orgID)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get organization repositories: %v", err)
|
||||
}
|
||||
|
||||
if len(orgRepos) == 0 {
|
||||
fmt.Printf("No repositories found for organization '%s'\n", *orgName)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d repositories in organization '%s'\n", len(orgRepos), *orgName)
|
||||
|
||||
// Build a map of organization repositories by name for quick lookup
|
||||
orgRepoMap := make(map[string]*Repository)
|
||||
for i, repo := range orgRepos {
|
||||
orgRepoMap[repo.Name] = &orgRepos[i]
|
||||
if *verbose {
|
||||
fmt.Printf("Org repo: %s (ID: %d)\n", repo.Name, repo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all user repositories
|
||||
userRepos, err := getUserRepos(db)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get user repositories: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d repositories in user namespaces\n", len(userRepos))
|
||||
|
||||
// Find matches and setup fork relationships
|
||||
matchCount := 0
|
||||
updateCount := 0
|
||||
|
||||
for _, userRepo := range userRepos {
|
||||
// Skip if the repo is already a fork
|
||||
if userRepo.IsFork && userRepo.ForkID.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if there's a matching org repo with the same name
|
||||
if orgRepo, exists := orgRepoMap[userRepo.Name]; exists {
|
||||
matchCount++
|
||||
|
||||
if *verbose || *dryRun {
|
||||
fmt.Printf("Match found: %s/%s (ID: %d) -> %s/%s (ID: %d)\n",
|
||||
userRepo.OwnerName, userRepo.Name, userRepo.ID,
|
||||
*orgName, orgRepo.Name, orgRepo.ID)
|
||||
}
|
||||
|
||||
// Update the fork relationship if not in dry run mode
|
||||
if !*dryRun {
|
||||
err := updateForkRelationship(db, userRepo.ID, orgRepo.ID)
|
||||
if err != nil {
|
||||
log.Printf("Error updating fork relationship for %s/%s: %v",
|
||||
userRepo.OwnerName, userRepo.Name, err)
|
||||
} else {
|
||||
updateCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nSummary:\n")
|
||||
fmt.Printf("- Matching repositories found: %d\n", matchCount)
|
||||
|
||||
if *dryRun {
|
||||
fmt.Printf("- Dry run mode: no changes made\n")
|
||||
} else {
|
||||
fmt.Printf("- Fork relationships updated: %d\n", updateCount)
|
||||
}
|
||||
|
||||
if !*dryRun {
|
||||
fmt.Printf("\nNOTE: You should restart your Gitea instance for changes to take effect.\n")
|
||||
}
|
||||
}
|
||||
|
||||
// printUsage displays the help information
|
||||
func printUsage() {
|
||||
fmt.Println("Gitea Organization Fork Matcher")
|
||||
fmt.Println("\nThis tool identifies repositories with the same name in both organization and user")
|
||||
fmt.Println("namespaces, then establishes proper fork relationships in Gitea's SQLite database.")
|
||||
fmt.Println("\nUsage:")
|
||||
fmt.Println(" gitea-org-fork-matcher -db /path/to/gitea.db -org YourOrgName")
|
||||
fmt.Println("\nOptions:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("\nNOTE: Always restart Gitea after making changes for them to take effect.")
|
||||
}
|
||||
|
||||
// backupDatabase creates a backup of the database file
|
||||
func backupDatabase(dbPath string) string {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
backupPath := dbPath + ".backup-" + timestamp
|
||||
|
||||
// Read the original database file
|
||||
data, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read database for backup: %v", err)
|
||||
}
|
||||
|
||||
// Write to the backup file
|
||||
err = os.WriteFile(backupPath, data, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create backup: %v", err)
|
||||
}
|
||||
|
||||
return backupPath
|
||||
}
|
||||
|
||||
// getOrganizationID retrieves the ID of an organization by name
|
||||
func getOrganizationID(db *sql.DB, orgName string) (int64, error) {
|
||||
var id int64
|
||||
query := `SELECT id FROM user WHERE name = ? AND type = ?`
|
||||
err := db.QueryRow(query, orgName, UserTypeOrganization).Scan(&id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, fmt.Errorf("organization '%s' not found", orgName)
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// getOrganizationRepos retrieves all repositories owned by the given organization
|
||||
func getOrganizationRepos(db *sql.DB, orgID int64) ([]Repository, error) {
|
||||
query := `
|
||||
SELECT r.id, r.name, r.owner_id, u.name AS owner_name, r.is_fork, r.fork_id
|
||||
FROM repository r
|
||||
JOIN user u ON r.owner_id = u.id
|
||||
WHERE r.owner_id = ?
|
||||
ORDER BY r.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var repos []Repository
|
||||
for rows.Next() {
|
||||
var repo Repository
|
||||
if err := rows.Scan(&repo.ID, &repo.Name, &repo.OwnerID, &repo.OwnerName, &repo.IsFork, &repo.ForkID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// getUserRepos retrieves all repositories owned by individual users (not organizations)
|
||||
func getUserRepos(db *sql.DB) ([]Repository, error) {
|
||||
query := `
|
||||
SELECT r.id, r.name, r.owner_id, u.name AS owner_name, r.is_fork, r.fork_id
|
||||
FROM repository r
|
||||
JOIN user u ON r.owner_id = u.id
|
||||
WHERE u.type = ?
|
||||
ORDER BY r.name, u.name
|
||||
`
|
||||
|
||||
rows, err := db.Query(query, UserTypeIndividual)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var repos []Repository
|
||||
for rows.Next() {
|
||||
var repo Repository
|
||||
if err := rows.Scan(&repo.ID, &repo.Name, &repo.OwnerID, &repo.OwnerName, &repo.IsFork, &repo.ForkID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
// updateForkRelationship sets a repository as a fork of another
|
||||
func updateForkRelationship(db *sql.DB, forkID, originalID int64) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %v", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec("UPDATE repository SET fork_id = ?, is_fork = 1 WHERE id = ?", originalID, forkID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("failed to update fork relationship: %v", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit changes: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enhanced organization listing function
|
||||
func listOrganizations(db *sql.DB) {
|
||||
// First, let's check if we can access the user table at all
|
||||
var tableExists int
|
||||
err := db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='user'").Scan(&tableExists)
|
||||
if err != nil {
|
||||
log.Fatalf("Error checking for user table: %v", err)
|
||||
}
|
||||
|
||||
if tableExists == 0 {
|
||||
log.Fatalf("The 'user' table does not exist in this database")
|
||||
}
|
||||
|
||||
// Let's examine the schema of the user table
|
||||
fmt.Println("User table schema:")
|
||||
rows, err := db.Query("PRAGMA table_info(user)")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query user table schema: %v", err)
|
||||
}
|
||||
|
||||
columns := make(map[string]string)
|
||||
fmt.Println("Column\tType")
|
||||
fmt.Println("------\t----")
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, ctype string
|
||||
var notnull, pk int
|
||||
var dfltValue interface{}
|
||||
|
||||
if err := rows.Scan(&cid, &name, &ctype, ¬null, &dfltValue, &pk); err != nil {
|
||||
log.Fatalf("Failed to scan schema row: %v", err)
|
||||
}
|
||||
columns[name] = ctype
|
||||
fmt.Printf("%s\t%s\n", name, ctype)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Check if the type column exists
|
||||
if _, hasType := columns["type"]; !hasType {
|
||||
// Check if it might be called something else, like "user_type"
|
||||
altTypeColumns := []string{"user_type", "kind", "organization_type"}
|
||||
for _, colName := range altTypeColumns {
|
||||
if _, has := columns[colName]; has {
|
||||
fmt.Printf("\nFound potential type column: '%s'\n", colName)
|
||||
}
|
||||
}
|
||||
log.Fatalf("The 'type' column does not exist in user table")
|
||||
}
|
||||
|
||||
// Let's see what type values actually exist in the database
|
||||
fmt.Println("\nDistinct user types in database:")
|
||||
typeRows, err := db.Query("SELECT DISTINCT type, COUNT(*) FROM user GROUP BY type")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query user types: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("Type\tCount")
|
||||
fmt.Println("----\t-----")
|
||||
typeFound := false
|
||||
for typeRows.Next() {
|
||||
var userType, count int
|
||||
if err := typeRows.Scan(&userType, &count); err != nil {
|
||||
log.Fatalf("Failed to scan type row: %v", err)
|
||||
}
|
||||
typeFound = true
|
||||
fmt.Printf("%d\t%d\n", userType, count)
|
||||
}
|
||||
typeRows.Close()
|
||||
|
||||
if !typeFound {
|
||||
fmt.Println("No user type data found - table might be empty or type field has NULL values")
|
||||
}
|
||||
|
||||
// Now try to get the list of organizations regardless of type
|
||||
fmt.Println("\nAttempting to list all users (potential organizations):")
|
||||
userRows, err := db.Query("SELECT id, name, type FROM user ORDER BY name LIMIT 20")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to query users: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("ID\tName\tType")
|
||||
fmt.Println("--\t----\t----")
|
||||
userFound := false
|
||||
for userRows.Next() {
|
||||
var id, userType int
|
||||
var name string
|
||||
if err := userRows.Scan(&id, &name, &userType); err != nil {
|
||||
log.Fatalf("Failed to scan user row: %v", err)
|
||||
}
|
||||
userFound = true
|
||||
fmt.Printf("%d\t%s\t%d\n", id, name, userType)
|
||||
}
|
||||
userRows.Close()
|
||||
|
||||
if !userFound {
|
||||
fmt.Println("No users found in the database")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user