feat(backend,ui): add repo project name

Ayman Bagabas created

Change summary

server/backend/file/file.go | 23 ++++++++++++++++++++++
server/backend/file/repo.go | 12 +++++++++-
server/backend/repo.go      |  6 +++++
server/cmd/cmd.go           |  1 
server/cmd/project_name.go  | 40 +++++++++++++++++++++++++++++++++++++++
ui/pages/repo/repo.go       |  6 ++++
ui/pages/selection/item.go  |  7 +++++
7 files changed, 91 insertions(+), 4 deletions(-)

Detailed changes

server/backend/file/file.go 🔗

@@ -49,6 +49,7 @@ const (
 	description  = "description"
 	exportOk     = "git-daemon-export-ok"
 	private      = "private"
+	projectName  = "project-name"
 	settings     = "settings"
 )
 
@@ -573,6 +574,23 @@ func (fb *FileBackend) SetPrivate(repo string, priv bool) error {
 	return nil
 }
 
+// ProjectName returns the project name.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) ProjectName(repo string) string {
+	repo = utils.SanitizeRepo(repo) + ".git"
+	r := &Repo{path: filepath.Join(fb.reposPath(), repo), root: fb.reposPath()}
+	return r.ProjectName()
+}
+
+// SetProjectName sets the project name of the given repo.
+//
+// It implements backend.Backend.
+func (fb *FileBackend) SetProjectName(repo string, name string) error {
+	repo = utils.SanitizeRepo(repo) + ".git"
+	return os.WriteFile(filepath.Join(fb.reposPath(), repo, projectName), []byte(name), 0600)
+}
+
 // CreateRepository creates a new repository.
 //
 // Created repositories are always bare.
@@ -607,6 +625,11 @@ func (fb *FileBackend) CreateRepository(repo string, private bool) (backend.Repo
 		return nil, err
 	}
 
+	if err := fb.SetProjectName(repo, name); err != nil {
+		logger.Debug("failed to set project name", "err", err)
+		return nil, err
+	}
+
 	r := &Repo{path: rp, root: fb.reposPath()}
 	// Add to cache.
 	fb.repos[name] = r

server/backend/file/repo.go 🔗

@@ -27,14 +27,22 @@ func (r *Repo) Name() string {
 	return strings.TrimPrefix(name, "/")
 }
 
+// ProjectName returns the repository's project name.
+func (r *Repo) ProjectName() string {
+	pn, err := readOneLine(filepath.Join(r.path, projectName))
+	if err != nil {
+		return ""
+	}
+
+	return strings.TrimSpace(pn)
+}
+
 // Description returns the repository's description.
 //
 // It implements backend.Repository.
 func (r *Repo) Description() string {
 	desc, err := readAll(filepath.Join(r.path, description))
 	if err != nil {
-		logger.Debug("failed to read description file", "err", err,
-			"path", filepath.Join(r.path, description))
 		return ""
 	}
 

server/backend/repo.go 🔗

@@ -21,6 +21,10 @@ type RepositoryStore interface {
 
 // RepositoryMetadata is an interface for managing repository metadata.
 type RepositoryMetadata interface {
+	// ProjectName returns the repository's project name.
+	ProjectName(repo string) string
+	// SetProjectName sets the repository's project name.
+	SetProjectName(repo, name string) error
 	// Description returns the repository's description.
 	Description(repo string) string
 	// SetDescription sets the repository's description.
@@ -57,6 +61,8 @@ type RepositoryAccess interface {
 type Repository interface {
 	// Name returns the repository's name.
 	Name() string
+	// ProjectName returns the repository's project name.
+	ProjectName() string
 	// Description returns the repository's description.
 	Description() string
 	// IsPrivate returns whether the repository is private.

server/cmd/cmd.go 🔗

@@ -64,6 +64,7 @@ func rootCommand() *cobra.Command {
 		hookCommand(),
 		listCommand(),
 		privateCommand(),
+		projectName(),
 		renameCommand(),
 		settingCommand(),
 		tagCommand(),

server/cmd/project_name.go 🔗

@@ -0,0 +1,40 @@
+package cmd
+
+import (
+	"strings"
+
+	"github.com/spf13/cobra"
+)
+
+func projectName() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:     "project-name REPOSITORY [NAME]",
+		Aliases: []string{"project"},
+		Short:   "Set or get the project name for a repository",
+		Args:    cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			cfg, _ := fromContext(cmd)
+			rn := strings.TrimSuffix(args[0], ".git")
+			switch len(args) {
+			case 1:
+				if err := checkIfReadable(cmd, args); err != nil {
+					return err
+				}
+
+				pn := cfg.Backend.ProjectName(rn)
+				cmd.Println(pn)
+			default:
+				if err := checkIfCollab(cmd, args); err != nil {
+					return err
+				}
+				if err := cfg.Backend.SetProjectName(rn, strings.Join(args[1:], " ")); err != nil {
+					return err
+				}
+			}
+
+			return nil
+		},
+	}
+
+	return cmd
+}

ui/pages/repo/repo.go 🔗

@@ -327,7 +327,11 @@ func (r *Repo) headerView() string {
 		return ""
 	}
 	truncate := lipgloss.NewStyle().MaxWidth(r.common.Width)
-	name := r.common.Styles.Repo.HeaderName.Render(r.selectedRepo.Name())
+	name := r.selectedRepo.ProjectName()
+	if name == "" {
+		name = r.selectedRepo.Name()
+	}
+	name = r.common.Styles.Repo.HeaderName.Render(name)
 	desc := r.selectedRepo.Description()
 	if desc == "" {
 		desc = name

ui/pages/selection/item.go 🔗

@@ -79,7 +79,12 @@ func (i Item) ID() string {
 
 // Title returns the item title. Implements list.DefaultItem.
 func (i Item) Title() string {
-	return i.repo.Name()
+	name := i.repo.ProjectName()
+	if name == "" {
+		name = i.repo.Name()
+	}
+
+	return name
 }
 
 // Description returns the item description. Implements list.DefaultItem.