can now import repos and have more settings for creating new ones.
This commit is contained in:
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
@@ -62,9 +63,12 @@ func (h *RepoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := middleware.UserIDFromContext(r.Context())
|
||||
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPrivate bool `json:"isPrivate"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPrivate bool `json:"isPrivate"`
|
||||
DefaultBranch string `json:"defaultBranch"`
|
||||
InitReadme string `json:"initReadme"` // "none" | "blank" | "tutorial"
|
||||
InitGitignore bool `json:"initGitignore"` // true → add .gitignore
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
@@ -74,6 +78,93 @@ func (h *RepoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, "name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
branch := body.DefaultBranch
|
||||
if branch == "" {
|
||||
branch = "main"
|
||||
}
|
||||
|
||||
diskPath := filepath.Join(h.cfg.RepoRoot, strconv.FormatInt(userID, 10), body.Name+".git")
|
||||
|
||||
repo := &models.Repository{
|
||||
OwnerID: userID,
|
||||
Name: body.Name,
|
||||
Description: body.Description,
|
||||
IsPrivate: body.IsPrivate,
|
||||
DefaultBranch: branch,
|
||||
DiskPath: diskPath,
|
||||
}
|
||||
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
if err := gitdomain.Init(diskPath); err != nil {
|
||||
jsonError(w, "could not initialise git repository", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update bare repo HEAD if branch differs from the Init default (main).
|
||||
if branch != "main" {
|
||||
gitdomain.SetDefaultBranch(diskPath, branch)
|
||||
}
|
||||
|
||||
if _, err := h.db.Insert(repo); err != nil {
|
||||
jsonError(w, "repository name already taken", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// Create initial commit when README or .gitignore was requested.
|
||||
wantsReadme := body.InitReadme == "blank" || body.InitReadme == "tutorial"
|
||||
if wantsReadme || body.InitGitignore {
|
||||
var u models.User
|
||||
h.db.ID(userID).Get(&u)
|
||||
authorEmail := u.Email
|
||||
if authorEmail == "" {
|
||||
authorEmail = u.Username + "@forgebucket.local"
|
||||
}
|
||||
if err := gitdomain.InitWithFiles(diskPath, branch, body.Name, u.Username, authorEmail, wantsReadme, body.InitGitignore); err != nil {
|
||||
// Non-fatal: repo is created, just without initial files.
|
||||
_ = err
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(h.withOwnerName(repo))
|
||||
}
|
||||
|
||||
func (h *RepoHandler) Import(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
jsonError(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IsPrivate bool `json:"isPrivate"`
|
||||
AuthUser string `json:"authUser"`
|
||||
AuthPass string `json:"authPass"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
jsonError(w, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.URL == "" || body.Name == "" {
|
||||
jsonError(w, "url and name are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Inject credentials into the URL if provided.
|
||||
srcURL := body.URL
|
||||
if body.AuthUser != "" {
|
||||
u, err := url.Parse(body.URL)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid URL", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u.User = url.UserPassword(body.AuthUser, body.AuthPass)
|
||||
srcURL = u.String()
|
||||
}
|
||||
|
||||
diskPath := filepath.Join(h.cfg.RepoRoot, strconv.FormatInt(userID, 10), body.Name+".git")
|
||||
|
||||
@@ -86,15 +177,15 @@ func (h *RepoHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
DiskPath: diskPath,
|
||||
}
|
||||
|
||||
// Initialise bare repo on disk before inserting to DB
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
if err := gitdomain.Init(diskPath); err != nil {
|
||||
jsonError(w, "could not initialise git repository", http.StatusInternalServerError)
|
||||
if _, err := h.db.Insert(repo); err != nil {
|
||||
jsonError(w, "repository name already taken", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.db.Insert(repo); err != nil {
|
||||
jsonError(w, "repository name already taken", http.StatusConflict)
|
||||
gitdomain.SetRepoRoot(h.cfg.RepoRoot)
|
||||
if err := gitdomain.CloneRepo(srcURL, diskPath); err != nil {
|
||||
h.db.ID(repo.ID).Delete(&models.Repository{})
|
||||
jsonError(w, "import failed: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, staticFi
|
||||
r.Route("/repos", func(r chi.Router) {
|
||||
r.Get("/", repoH.List)
|
||||
r.With(csrf).Post("/", repoH.Create)
|
||||
r.With(csrf).Post("/import", repoH.Import)
|
||||
r.Route("/{owner}/{repo}", func(r chi.Router) {
|
||||
r.Get("/", repoH.Get)
|
||||
r.With(csrf).Patch("/", repoH.Update)
|
||||
|
||||
@@ -271,6 +271,106 @@ func Branches(repoPath string) ([]Branch, error) {
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
// SetDefaultBranch updates HEAD to point at the given branch name.
|
||||
func SetDefaultBranch(repoPath, branch string) error {
|
||||
_, err := run(repoPath, "symbolic-ref", "HEAD", "refs/heads/"+branch)
|
||||
return err
|
||||
}
|
||||
|
||||
// InitWithFiles creates an initial commit in a bare repo by:
|
||||
// 1. git init in a temp dir → write files → commit → git push file://repoPath.
|
||||
// Use this right after Init() when initReadme or initGitignore is requested.
|
||||
func InitWithFiles(repoPath, branch, repoName, authorName, authorEmail string, initReadme, initGitignore bool) error {
|
||||
tmpDir, err := os.MkdirTemp("", "fb-init-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("mktemp: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
env := []string{
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"HOME=/tmp",
|
||||
"GIT_AUTHOR_NAME=" + authorName,
|
||||
"GIT_AUTHOR_EMAIL=" + authorEmail,
|
||||
"GIT_COMMITTER_NAME=" + authorName,
|
||||
"GIT_COMMITTER_EMAIL=" + authorEmail,
|
||||
}
|
||||
|
||||
initCmd := exec.Command("git", "init", tmpDir)
|
||||
initCmd.Env = env
|
||||
if out, err := initCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git init: %w: %s", err, out)
|
||||
}
|
||||
|
||||
// Point the working-tree HEAD at the desired branch before the first commit.
|
||||
symRef := exec.Command("git", "-C", tmpDir, "symbolic-ref", "HEAD", "refs/heads/"+branch)
|
||||
symRef.Env = env
|
||||
symRef.Run()
|
||||
|
||||
if initReadme {
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte(readmeContent(repoName)), 0644); err != nil {
|
||||
return fmt.Errorf("write README: %w", err)
|
||||
}
|
||||
}
|
||||
if initGitignore {
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, ".gitignore"), []byte(gitignoreContent()), 0644); err != nil {
|
||||
return fmt.Errorf("write .gitignore: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
addCmd := exec.Command("git", "add", ".")
|
||||
addCmd.Dir = tmpDir
|
||||
addCmd.Env = env
|
||||
if out, err := addCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git add: %w: %s", err, out)
|
||||
}
|
||||
|
||||
commitCmd := exec.Command("git", "commit", "-m", "Initial commit")
|
||||
commitCmd.Dir = tmpDir
|
||||
commitCmd.Env = env
|
||||
if out, err := commitCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git commit: %w: %s", err, out)
|
||||
}
|
||||
|
||||
// Push to the bare repo via file:// — works on all git versions.
|
||||
pushCmd := exec.Command("git", "push", "file://"+filepath.ToSlash(filepath.Clean(repoPath)), branch)
|
||||
pushCmd.Dir = tmpDir
|
||||
pushCmd.Env = env
|
||||
if out, err := pushCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("git push: %w: %s", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneRepo mirrors a remote HTTP/HTTPS git repository into destPath (bare).
|
||||
func CloneRepo(srcURL, destPath string) error {
|
||||
if !strings.HasPrefix(srcURL, "http://") && !strings.HasPrefix(srcURL, "https://") {
|
||||
return errors.New("only http and https source URLs are supported")
|
||||
}
|
||||
env := []string{"GIT_TERMINAL_PROMPT=0", "HOME=/tmp"}
|
||||
|
||||
cmd := exec.Command("git", "clone", "--mirror", srcURL, destPath)
|
||||
cmd.Env = env
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
// Scrub the URL (which may contain credentials) from the error.
|
||||
msg := strings.ReplaceAll(string(out), srcURL, "<url>")
|
||||
return fmt.Errorf("git clone: %w: %s", err, msg)
|
||||
}
|
||||
// Enable pushes over HTTP.
|
||||
cfgCmd := exec.Command("git", "-C", destPath, "config", "http.receivepack", "true")
|
||||
cfgCmd.Env = env
|
||||
cfgCmd.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readmeContent(repoName string) string {
|
||||
return fmt.Sprintf("# %s\n\n## Description\n\nAdd your project description here.\n\n## Getting Started\n\n### Prerequisites\n\nList your prerequisites here.\n\n### Installation\n\n1. Clone the repository\n2. Follow setup instructions\n\n## Usage\n\nDescribe how to use your project.\n\n## Contributing\n\nContributions are welcome! Please open an issue or pull request.\n\n## License\n\nThis project is licensed under the MIT License.\n", repoName)
|
||||
}
|
||||
|
||||
func gitignoreContent() string {
|
||||
return "# OS\n.DS_Store\nThumbs.db\n\n# Editor\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# Logs\n*.log\nlogs/\n\n# Dependencies\nnode_modules/\nvendor/\n\n# Build artifacts\ndist/\nbuild/\n*.o\n*.a\n*.so\n"
|
||||
}
|
||||
|
||||
func BlobCat(repoPath, ref, filePath string) ([]byte, error) {
|
||||
return run(repoPath, "show", ref+":"+filePath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user