mirror of
https://github.com/go-i2p/gitlab-to-gitea.git
synced 2025-08-19 15:45:26 -04:00
Compare commits
7 Commits
10c6d93cb8
...
f62322aee4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f62322aee4 | ||
![]() |
710e18811b | ||
![]() |
528e14eb05 | ||
![]() |
72a50f823a | ||
![]() |
2ab3bc115c | ||
![]() |
3c0c74cfb1 | ||
![]() |
e6d170f16f |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -27,5 +27,9 @@ go.work.sum
|
||||
/unmigrate
|
||||
/forkfix
|
||||
/migration_state.json
|
||||
/bin
|
||||
log.log
|
||||
err.log
|
||||
err.log
|
||||
i2pkeys
|
||||
onionkeys
|
||||
tlskeys
|
31
Makefile
31
Makefile
@@ -3,11 +3,30 @@ fmt:
|
||||
@echo "Running gofumpt..."
|
||||
find . -name '*.go' -exec gofumpt -w -s -extra {} \;
|
||||
|
||||
migrate:
|
||||
go build -o migrate ./cmd/migrate
|
||||
CGO_ENABLED=0
|
||||
|
||||
unmigrate:
|
||||
go build -o unmigrate ./cmd/unmigrate
|
||||
all: bin migrate unmigrate forkfix orgfix namefix
|
||||
|
||||
forkfix:
|
||||
go build -o forkfix ./cmd/forkfix
|
||||
bin:
|
||||
mkdir -p ./bin
|
||||
|
||||
migrate: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/migrate ./cmd/migrate
|
||||
|
||||
unmigrate: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/unmigrate ./cmd/unmigrate
|
||||
|
||||
forkfix: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/forkfix ./cmd/forkfix
|
||||
|
||||
orgfix: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/orgfix ./cmd/orgfix
|
||||
|
||||
mirror: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/mirror ./cmd/mirror
|
||||
|
||||
namefix: bin
|
||||
go build --tags=netgo,osusergo -o ./bin/namefix ./cmd/namefix
|
||||
|
||||
clean:
|
||||
rm -f ./bin/migrate ./bin/unmigrate ./bin/forkfix ./bin/orgfix
|
10
README.md
10
README.md
@@ -4,7 +4,13 @@ Go-based tool for migrating GitLab repositories, users, groups, issues and relat
|
||||
|
||||
More-or-less a port of [gitlab-to-gitea](https://git.autonomic.zone/kawaiipunk/gitlab-to-gitea) from python to Go because *fixing* python appears to be a thing I just can't get my mind around, but *rewriting* it? I'm actually OK at that.
|
||||
|
||||
Also includes: `cmd/forkfix`, for fixing fork relationships between migrated repositories by manipulating the gitea mysql database, `cmd/unmigrate` to delete everything from a gitea instance except for the admin users, and `cmd/johnconnor` which is a super-dangerous script for eliminating spam accounts from gitlab instances.
|
||||
Also includes:
|
||||
- `cmd/forkfix`, for fixing fork relationships between migrated repositories by manipulating the gitea sql database
|
||||
- `cmd/unmigrate` to delete everything from a gitea instance except for the admin users
|
||||
- `cmd/mirror` to set up a mirror from github to gitea
|
||||
- `cmd/orgfix` to establish an organization's repositories as the "parent fork" by manipulating the gitea sql database
|
||||
- `cmd/namefix` to discover repositories that have identical initial commit hashes but different names
|
||||
- `cmd/johnconnor` which is a super-dangerous script for eliminating spam accounts from gitlab instances.
|
||||
|
||||
## Core Functionality
|
||||
|
||||
@@ -18,7 +24,7 @@ Also includes: `cmd/forkfix`, for fixing fork relationships between migrated rep
|
||||
|
||||
- Modular package structure instead of monolithic script
|
||||
- Configuration via environment variables rather than hardcoded values
|
||||
- Added utility tools (`forkfix`, `unmigrate`, `johnconnor`)
|
||||
- Added utility tools (`forkfix`, `unmigrate`, `johnconnor`, `mirror`, `orgfix`, `namefix`)
|
||||
- Database connectivity for commit action imports
|
||||
- Improved error handling with recovery mechanisms
|
||||
- Separation of API client code from migration logic
|
||||
|
250
cmd/mirror/main.go
Normal file
250
cmd/mirror/main.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/gitlab-to-gitea/config"
|
||||
"github.com/go-i2p/gitlab-to-gitea/gitea"
|
||||
"github.com/go-i2p/gitlab-to-gitea/utils"
|
||||
"github.com/google/go-github/v57/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
scriptVersion = "1.0.0"
|
||||
)
|
||||
|
||||
// Repository types for processing
|
||||
type RepoInfo struct {
|
||||
Name string
|
||||
FullName string
|
||||
Description string
|
||||
CloneURL string
|
||||
HTMLURL string
|
||||
IsPrivate bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
utils.PrintHeader("---=== GitHub to Gitea Repository Mirror ===---")
|
||||
fmt.Printf("Version: %s\n\n", scriptVersion)
|
||||
|
||||
// Define command line flags
|
||||
githubAccount := flag.String("account", "", "GitHub username or organization name (required)")
|
||||
isOrg := flag.Bool("org", false, "Treat the account as an organization")
|
||||
githubToken := flag.String("github-token", "", "GitHub personal access token (optional but recommended to avoid rate limits)")
|
||||
targetOwner := flag.String("target-owner", "", "Gitea account where repositories will be created (defaults to current user)")
|
||||
includePrivate := flag.Bool("include-private", false, "Include private repositories (requires authentication)")
|
||||
help := flag.Bool("help", false, "Show usage information")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Show help if requested or required args missing
|
||||
if *help || *githubAccount == "" {
|
||||
showUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// Load environment variables
|
||||
err := config.LoadEnv()
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to load environment variables: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to load configuration: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize GitHub client
|
||||
githubClient := createGitHubClient(*githubToken)
|
||||
|
||||
// Initialize Gitea client
|
||||
giteaClient, err := gitea.NewClient(cfg.GiteaURL, cfg.GiteaToken)
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to connect to Gitea: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Verify connections
|
||||
gtVersion, err := giteaClient.GetVersion()
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to get Gitea version: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
utils.PrintInfo(fmt.Sprintf("Connected to Gitea, version: %s", gtVersion))
|
||||
|
||||
// Get Gitea current user if target owner not specified
|
||||
if *targetOwner == "" {
|
||||
var currentUser map[string]interface{}
|
||||
err = giteaClient.Get("user", ¤tUser)
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to get current user: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
*targetOwner = currentUser["username"].(string)
|
||||
utils.PrintInfo(fmt.Sprintf("Target owner set to current user: %s", *targetOwner))
|
||||
}
|
||||
|
||||
// Get GitHub repositories
|
||||
repos, err := getGitHubRepositories(githubClient, *githubAccount, *isOrg, *includePrivate)
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to get GitHub repositories: %v", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(repos) == 0 {
|
||||
utils.PrintWarning(fmt.Sprintf("No repositories found for %s", *githubAccount))
|
||||
return
|
||||
}
|
||||
|
||||
utils.PrintInfo(fmt.Sprintf("Found %d repositories for %s", len(repos), *githubAccount))
|
||||
|
||||
// Mirror repositories to Gitea
|
||||
mirrorRepositories(giteaClient, repos, *targetOwner)
|
||||
}
|
||||
|
||||
// showUsage displays the help information
|
||||
func showUsage() {
|
||||
fmt.Println("GitHub to Gitea Repository Mirror")
|
||||
fmt.Println("\nThis tool mirrors GitHub repositories to a Gitea instance.")
|
||||
fmt.Println("\nUsage:")
|
||||
fmt.Println(" mirror -account <username> [-org] [-github-token <token>] [-target-owner <owner>] [-include-private]")
|
||||
fmt.Println("\nOptions:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("\nNOTE: GitHub has API rate limits - 60 requests/hour for unauthenticated requests, 5000 requests/hour with a token.")
|
||||
}
|
||||
|
||||
// createGitHubClient initializes a GitHub API client
|
||||
func createGitHubClient(token string) *github.Client {
|
||||
ctx := context.Background()
|
||||
if token == "" {
|
||||
// Unauthenticated client (rate limited to 60 requests/hour)
|
||||
return github.NewClient(nil)
|
||||
}
|
||||
|
||||
// Authenticated client (rate limited to 5000 requests/hour)
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
return github.NewClient(tc)
|
||||
}
|
||||
|
||||
// getGitHubRepositories fetches repositories from GitHub
|
||||
func getGitHubRepositories(client *github.Client, account string, isOrg, includePrivate bool) ([]RepoInfo, error) {
|
||||
ctx := context.Background()
|
||||
var allRepos []RepoInfo
|
||||
|
||||
var page int = 1
|
||||
|
||||
for {
|
||||
var (
|
||||
repos []*github.Repository
|
||||
resp *github.Response
|
||||
err error
|
||||
)
|
||||
|
||||
if isOrg {
|
||||
orgOpts := &github.RepositoryListByOrgOptions{
|
||||
ListOptions: github.ListOptions{PerPage: 100, Page: page},
|
||||
}
|
||||
repos, resp, err = client.Repositories.ListByOrg(ctx, account, orgOpts)
|
||||
} else {
|
||||
opts := &github.RepositoryListOptions{
|
||||
ListOptions: github.ListOptions{PerPage: 100, Page: page},
|
||||
}
|
||||
repos, resp, err = client.Repositories.List(ctx, account, opts)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert and filter repositories
|
||||
for _, repo := range repos {
|
||||
// Skip private repos if not explicitly included
|
||||
if *repo.Private && !includePrivate {
|
||||
continue
|
||||
}
|
||||
|
||||
allRepos = append(allRepos, RepoInfo{
|
||||
Name: *repo.Name,
|
||||
FullName: *repo.FullName,
|
||||
Description: stringOrEmpty(repo.Description),
|
||||
CloneURL: *repo.CloneURL,
|
||||
HTMLURL: *repo.HTMLURL,
|
||||
})
|
||||
}
|
||||
// Check if we need to get more pages
|
||||
if resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
// just do the first 100 for now.
|
||||
break
|
||||
}
|
||||
|
||||
return allRepos, nil
|
||||
}
|
||||
|
||||
// mirrorRepositories creates mirror repositories in Gitea
|
||||
func mirrorRepositories(client *gitea.Client, repos []RepoInfo, targetOwner string) {
|
||||
var (
|
||||
succeeded int
|
||||
failed int
|
||||
)
|
||||
|
||||
utils.PrintHeader(fmt.Sprintf("Starting mirror process for %d repositories...", len(repos)))
|
||||
|
||||
for _, repo := range repos {
|
||||
utils.PrintInfo(fmt.Sprintf("Mirroring %s...", repo.FullName))
|
||||
|
||||
// Create the mirror repository in Gitea
|
||||
mirrorData := map[string]interface{}{
|
||||
"clone_addr": repo.CloneURL,
|
||||
"repo_name": repo.Name,
|
||||
"mirror": true,
|
||||
"private": repo.IsPrivate,
|
||||
"description": repo.Description,
|
||||
"repo_owner": targetOwner,
|
||||
"service": "git",
|
||||
"wiki": true,
|
||||
"issues": true,
|
||||
"labels": true,
|
||||
"milestones": true,
|
||||
"releases": true,
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err := client.Post("repos/migrate", mirrorData, &result)
|
||||
|
||||
if err != nil {
|
||||
utils.PrintError(fmt.Sprintf("Failed to mirror %s: %v", repo.FullName, err))
|
||||
failed++
|
||||
} else {
|
||||
utils.PrintSuccess(fmt.Sprintf("Successfully mirrored %s", repo.FullName))
|
||||
succeeded++
|
||||
}
|
||||
|
||||
// Avoid hitting rate limits
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
utils.PrintInfo(fmt.Sprintf("Mirror summary: %d succeeded, %d failed", succeeded, failed))
|
||||
}
|
||||
|
||||
// Helper function for nil string pointers
|
||||
func stringOrEmpty(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
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")
|
||||
}
|
||||
}
|
@@ -13,8 +13,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/gitlab-to-gitea/utils"
|
||||
)
|
||||
|
||||
// Client handles communication with the Gitea API
|
||||
@@ -82,6 +80,9 @@ func NewClient(baseURL, token string) (*Client, error) {
|
||||
baseURL: u,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 360 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: Dial,
|
||||
},
|
||||
},
|
||||
token: token,
|
||||
}, nil
|
||||
@@ -161,7 +162,7 @@ func (c *Client) request(method, path string, data, result interface{}) (*http.R
|
||||
fullURL := fmt.Sprintf("%s/%s", strings.TrimSuffix(c.baseURL.String(), "/"), path)
|
||||
|
||||
// Debug output to see what endpoint is being called
|
||||
utils.PrintInfo(fmt.Sprintf("Making %s request to: %s", method, fullURL))
|
||||
// utils.PrintInfo(fmt.Sprintf("Making %s request to: %s", method, fullURL))
|
||||
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
@@ -207,3 +208,8 @@ func (c *Client) request(method, path string, data, result interface{}) (*http.R
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetToken returns the authentication token
|
||||
func (c *Client) GetToken() string {
|
||||
return c.token
|
||||
}
|
||||
|
26
gitea/dialer.go
Normal file
26
gitea/dialer.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
metadialer "github.com/go-i2p/go-meta-dialer"
|
||||
)
|
||||
|
||||
func Dial(network, addr string) (net.Conn, error) {
|
||||
if metadialer.ANON {
|
||||
metadialer.ANON = false
|
||||
}
|
||||
return metadialer.Dial(network, addr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Initialize the client with a default timeout
|
||||
http.DefaultClient = &http.Client{
|
||||
Timeout: 360 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: Dial,
|
||||
},
|
||||
}
|
||||
}
|
26
gitlab/dialer.go
Normal file
26
gitlab/dialer.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gitlab
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
metadialer "github.com/go-i2p/go-meta-dialer"
|
||||
)
|
||||
|
||||
func Dial(network, addr string) (net.Conn, error) {
|
||||
if metadialer.ANON {
|
||||
metadialer.ANON = false
|
||||
}
|
||||
return metadialer.Dial(network, addr)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Initialize the client with a default timeout
|
||||
http.DefaultClient = &http.Client{
|
||||
Timeout: 360 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: Dial,
|
||||
},
|
||||
}
|
||||
}
|
13
go.mod
13
go.mod
@@ -3,20 +3,29 @@ module github.com/go-i2p/gitlab-to-gitea
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/go-i2p/go-meta-dialer v0.0.0-20250501024057-715e91be3cfe
|
||||
github.com/go-i2p/onramp v0.33.92
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/google/go-github/v57 v57.0.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/xanzy/go-gitlab v0.115.0
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/cretz/bine v0.2.0 // indirect
|
||||
github.com/go-i2p/i2pkeys v0.33.10-0.20241113193422-e10de5e60708 // indirect
|
||||
github.com/go-i2p/sam3 v0.33.9 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.29.1 // indirect
|
||||
|
47
go.sum
47
go.sum
@@ -1,9 +1,21 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/go-i2p/go-meta-dialer v0.0.0-20250501024057-715e91be3cfe h1:9Rxw2KtMCRKZHI4WavUAaatzKmc64V6kiYvcyTMHjeU=
|
||||
github.com/go-i2p/go-meta-dialer v0.0.0-20250501024057-715e91be3cfe/go.mod h1:++xHSOvnGymRSyFbi9A9hztcfwKfU6/nJAtVxrNo8Zo=
|
||||
github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=
|
||||
github.com/go-i2p/i2pkeys v0.33.10-0.20241113193422-e10de5e60708 h1:Tiy9IBwi21maNpK74yCdHursJJMkyH7w87tX1nXGWzg=
|
||||
github.com/go-i2p/i2pkeys v0.33.10-0.20241113193422-e10de5e60708/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=
|
||||
github.com/go-i2p/onramp v0.33.92 h1:Dk3A0SGpdEw829rSjW2LqN8o16pUvuhiN0vn36z7Gpc=
|
||||
github.com/go-i2p/onramp v0.33.92/go.mod h1:5sfB8H2xk05gAS2K7XAUZ7ekOfwGJu3tWF0fqdXzJG4=
|
||||
github.com/go-i2p/sam3 v0.33.9 h1:3a+gunx75DFc6jxloUZTAVJbdP6736VU1dy2i7I9fKA=
|
||||
github.com/go-i2p/sam3 v0.33.9/go.mod h1:oDuV145l5XWKKafeE4igJHTDpPwA0Yloz9nyKKh92eo=
|
||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -12,8 +24,10 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs=
|
||||
github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
@@ -32,21 +46,36 @@ github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEu
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
|
||||
github.com/xanzy/go-gitlab v0.115.0/go.mod h1:5XCDtM7AM6WMKmfDdOiEpyRWUqui2iS9ILfvCZ2gJ5M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -57,5 +86,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@@ -28,7 +28,8 @@ func ExtractUserMentions(text string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
// Ignore everything we found and return nothing
|
||||
return []string{""}
|
||||
}
|
||||
|
||||
// NormalizeMentions replaces all @mentions in text with their normalized versions
|
||||
|
Reference in New Issue
Block a user