diff --git a/.gitignore b/.gitignore index c795b05..d76b74e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -build \ No newline at end of file +build +.DS_Store diff --git a/cmd/git-remote-gitopia/gitopia.go b/cmd/git-remote-gitopia/gitopia.go index 7d1619b..a868090 100644 --- a/cmd/git-remote-gitopia/gitopia.go +++ b/cmd/git-remote-gitopia/gitopia.go @@ -98,6 +98,14 @@ func (h *GitopiaHandler) Initialize(remote *core.Remote) error { h.remoteRepository = *res.Repository + // Configure LFS URL for clone operations to avoid SSH to non-existent "gitopia" hostname + lfsURL := fmt.Sprintf("%v/%v.git", config.GitServerHost, h.remoteRepository.Id) + cmd := core.GitCommand("git", "config", "--local", "lfs.url", lfsURL) + if err := cmd.Run(); err != nil { + // Log but don't fail if LFS config fails (repo might not have LFS) + remote.Logger.Printf("Warning: could not configure LFS URL: %v", err) + } + return nil } @@ -114,8 +122,25 @@ func (h *GitopiaHandler) List(remote *core.Remote, forPush bool) ([]string, erro if err != nil { return out, err } + + // Track available branches and find a suitable HEAD candidate + var headCandidate string + defaultBranchExists := false + for _, branch := range branchAllRes.Branch { out = append(out, fmt.Sprintf("%s %s%s", branch.Sha, branchPrefix, branch.Name)) + + // Check if the configured default branch exists + if branch.Name == h.remoteRepository.DefaultBranch { + defaultBranchExists = true + headCandidate = branch.Name + } else if headCandidate == "" { + // Fallback: use the first branch we find + headCandidate = branch.Name + } else if branch.Name == "main" { + // Prefer "main" over other branches as fallback + headCandidate = branch.Name + } } tagAllRes, err := h.queryClient.RepositoryTagAll(context.Background(), &gitopiatypes.QueryAllRepositoryTagRequest{ @@ -132,51 +157,57 @@ func (h *GitopiaHandler) List(remote *core.Remote, forPush bool) ([]string, erro out = append(out, fmt.Sprintf("%s %s%s", tag.Sha, tagPrefix, tag.Name)) } - out = append(out, fmt.Sprintf("@refs/heads/%s HEAD", h.remoteRepository.DefaultBranch)) + // Only set HEAD if we have a valid branch to point to + if headCandidate != "" { + if !defaultBranchExists && headCandidate != h.remoteRepository.DefaultBranch { + // Log a warning if we're falling back to a different branch + remote.Logger.Printf("Warning: configured default branch '%s' doesn't exist, using '%s' as HEAD\n", + h.remoteRepository.DefaultBranch, headCandidate) + } + out = append(out, fmt.Sprintf("@refs/heads/%s HEAD", headCandidate)) + } else { + // No branches exist at all - this is a serious issue + remote.Logger.Printf("Warning: repository has no branches, HEAD will not be set\n") + } return out, nil } func (h *GitopiaHandler) Fetch(remote *core.Remote, refsToFetch []core.RefToFetch) error { remoteURL := fmt.Sprintf("%v/%v.git", config.GitServerHost, h.remoteRepository.Id) + lfsURL := remoteURL // Use same URL for LFS - if !remote.Force { - args := []string{ - "fetch", - "--no-write-fetch-head", - remoteURL, - } - for _, ref := range refsToFetch { - args = append(args, ref.Ref) - } - cmd := core.GitCommand("git", args...) - if err := cmd.Run(); err != nil { - return errors.Wrap(err, "error fetching from remote repository") + // Check if any refs need force + needsForce := false + var processedRefs []string + + for _, ref := range refsToFetch { + refSpec := ref.Ref + if strings.HasPrefix(refSpec, "+") { + refSpec = strings.TrimPrefix(refSpec, "+") + needsForce = true } + processedRefs = append(processedRefs, refSpec) + } - return nil + // Build single git fetch command with all refs + args := []string{ + "-c", fmt.Sprintf("lfs.url=%s", lfsURL), + "fetch", + "--no-write-fetch-head", } - for _, ref := range refsToFetch { - force := false - if strings.HasPrefix(ref.Ref, "+") { - ref.Ref = strings.TrimPrefix(ref.Ref, "+") - force = true - } + // Add force flag if any ref needs it + if needsForce || remote.Force { + args = append(args, "--force") + } - args := []string{ - "fetch", - "--no-write-fetch-head", - remoteURL, - ref.Ref, - } - if force { - args = append(args, "--force") - } - cmd := core.GitCommand("git", args...) - if err := cmd.Run(); err != nil { - return errors.Wrap(err, "error fetching from remote repository") - } + args = append(args, remoteURL) + args = append(args, processedRefs...) + + cmd := core.GitCommand("git", args...) + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "error fetching from remote repository") } return nil @@ -215,6 +246,7 @@ func (h *GitopiaHandler) Push(remote *core.Remote, refsToPush []core.RefToPush) } remoteURL := fmt.Sprintf("%v/%v.git", config.GitServerHost, h.remoteRepository.Id) + lfsURL := remoteURL // Use same URL for LFS var newRemoteRefSha string var setBranches []gitopiatypes.MsgMultiSetBranch_Branch @@ -267,6 +299,8 @@ func (h *GitopiaHandler) Push(remote *core.Remote, refsToPush []core.RefToPush) "credential.helper=", "-c", "credential.helper=gitopia", + "-c", + fmt.Sprintf("lfs.url=%s", lfsURL), "push", remoteURL, fmt.Sprintf("%s:%s", ref.Local, ref.Remote), diff --git a/core/util.go b/core/util.go index e3febaf..ec502ef 100644 --- a/core/util.go +++ b/core/util.go @@ -2,6 +2,7 @@ package core import ( "errors" + "fmt" "os" "os/exec" "path" @@ -52,31 +53,47 @@ func CleanUpProcessGroup(cmd *exec.Cmd) { } func ValidateGitopiaRemoteURL(remoteURL string) (remoteUserId string, remoteRepositoryName string, err error) { - if strings.HasPrefix(remoteURL, GITOPIA_PREFIX) { - s := strings.TrimPrefix(remoteURL, GITOPIA_PREFIX) - sp := strings.Split(s, "/") + if !strings.HasPrefix(remoteURL, GITOPIA_PREFIX) { + return "", "", fmt.Errorf("invalid gitopia remote url: must start with '%s', got '%s'", GITOPIA_PREFIX, remoteURL) + } + + s := strings.TrimPrefix(remoteURL, GITOPIA_PREFIX) + sp := strings.Split(s, "/") + + if len(sp) != 2 { + return "", "", fmt.Errorf("invalid gitopia remote url: expected format 'gitopia://user/repository', got '%s' (found %d parts after prefix)", remoteURL, len(sp)) + } + + remoteUserId = sp[0] + remoteRepositoryName = sp[1] - if len(sp) != 2 { - return "", "", ErrInvalidGitopiaRemoteURL + if remoteUserId == "" { + return "", "", fmt.Errorf("invalid gitopia remote url: user ID cannot be empty in '%s'", remoteURL) + } + + if remoteRepositoryName == "" { + return "", "", fmt.Errorf("invalid gitopia remote url: repository name cannot be empty in '%s'", remoteURL) + } + + // Try to parse as bech32 address first + _, err = sdk.AccAddressFromBech32(remoteUserId) + if err != nil { + // If not a valid bech32 address, validate as username + if len(remoteUserId) < 3 { + return "", "", fmt.Errorf("invalid gitopia remote url: user ID '%s' is too short (minimum 3 characters)", remoteUserId) + } + if len(remoteUserId) > 39 { + return "", "", fmt.Errorf("invalid gitopia remote url: user ID '%s' is too long (maximum 39 characters)", remoteUserId) + } + + valid, regexErr := regexp.MatchString("^[a-zA-Z0-9]+(?:[-]?[a-zA-Z0-9])*$", remoteUserId) + if regexErr != nil { + return "", "", fmt.Errorf("invalid gitopia remote url: error validating user ID '%s': %v", remoteUserId, regexErr) } - remoteUserId = sp[0] - remoteRepositoryName = sp[1] - - _, err := sdk.AccAddressFromBech32(remoteUserId) - if err != nil { - if len(remoteUserId) < 3 || len(remoteUserId) > 39 { - return "", "", ErrInvalidGitopiaRemoteURL - } - valid, err := regexp.MatchString("^[a-zA-Z0-9]+(?:[-]?[a-zA-Z0-9])*$", remoteUserId) - if err != nil { - return "", "", ErrInvalidGitopiaRemoteURL - } - if !valid { - return "", "", ErrInvalidGitopiaRemoteURL - } + if !valid { + return "", "", fmt.Errorf("invalid gitopia remote url: user ID '%s' contains invalid characters (only alphanumeric and hyphens allowed)", remoteUserId) } - return remoteUserId, remoteRepositoryName, nil } - return "", "", ErrInvalidGitopiaRemoteURL + return remoteUserId, remoteRepositoryName, nil }