Skip to content

Commit e1fa7ed

Browse files
committed
refactor: change logic
1 parent a14b7b8 commit e1fa7ed

File tree

9 files changed

+2403
-713
lines changed

9 files changed

+2403
-713
lines changed

cmd/helpers.go

Lines changed: 202 additions & 179 deletions
Large diffs are not rendered by default.

cmd/project_create.go

Lines changed: 102 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2,158 +2,153 @@ package cmd
22

33
import (
44
"encoding/json"
5-
"errors"
65
"fmt"
76
"os"
87
"os/exec"
98
"path/filepath"
109
"strconv"
1110
"strings"
11+
"sync"
1212

1313
"github.com/spf13/cobra"
1414
)
1515

1616
var (
17-
projectName string
18-
projectCount int
19-
projectPrefix string
20-
projectProto string
17+
createProjectCount int
18+
createProjectPrefix string
19+
createProjectProto string
2120
)
2221

2322
var projectCreateCmd = &cobra.Command{
24-
Use: "create",
25-
Short: "Create multiple projects with prefix and count",
26-
Long: `Create one or more new projects in the current subgroup on GitLab.
27-
28-
Use the --count flag to specify how many projects to create,
29-
and --prefix to define the project name prefix.
23+
Use: "create [names...]",
24+
Short: "Create projects (Interactive or Batch)",
25+
Long: `Create one or more projects in the current subgroup.
3026
3127
Examples:
32-
ash project create --count 5 --prefix Baitap
33-
→ Creates projects: Baitap1, Baitap2, Baitap3, Baitap4, Baitap5
34-
35-
All created projects will also be recorded in subgroup.json for sync tracking.`,
36-
SilenceUsage: true,
37-
SilenceErrors: true,
28+
ash project create BaiTap1 BaiTap2
29+
ash project create -c 5 -p Lab`,
30+
SilenceUsage: true,
3831

3932
RunE: func(cmd *cobra.Command, args []string) error {
40-
if projectProto != "ssh" && projectProto != "https" {
41-
projectProto = "https"
42-
}
43-
33+
// 1. Env Check
4434
wd, err := os.Getwd()
4535
if err != nil {
46-
return fmt.Errorf("getwd failed: %w", err)
36+
return err
4737
}
4838
ashDir := filepath.Join(wd, ".ash")
4939
subMetaPath := filepath.Join(ashDir, "subgroup.json")
40+
5041
if !fileExists(subMetaPath) {
51-
return errors.New("not in a subgroup folder: .ash/subgroup.json not found")
42+
return fmt.Errorf("not in a subgroup folder (.ash/subgroup.json missing)")
5243
}
5344

54-
// read subgroup meta to get current subgroupo (namespace)
5545
var meta subgroupMeta
5646
if err := readJSON(subMetaPath, &meta); err != nil {
57-
return fmt.Errorf("parse subgroup.json failed: %w", err)
58-
}
59-
if meta.Group.ID == 0 {
60-
return errors.New("invalid subgroup.json: missing subgroup id")
47+
return fmt.Errorf("read metadata failed: %w", err)
6148
}
6249

63-
// build names to create
50+
// 2. Determine List
6451
var names []string
65-
if strings.TrimSpace(projectName) != "" {
66-
if projectCount > 0 || strings.TrimSpace(projectPrefix) != "" {
67-
return errors.New("use either -n <Name> or -c <N> -p <prefix> ")
68-
}
69-
names = []string{projectName}
52+
if len(args) > 0 {
53+
names = args
7054
} else {
71-
if projectCount <= 0 || strings.TrimSpace(projectPrefix) == "" {
72-
return errors.New("missing project count, please use -c <N>")
55+
if createProjectCount <= 0 || createProjectPrefix == "" {
56+
return fmt.Errorf("missing args: usage 'ash project create Name' or '-c 5 -p Prefix'")
7357
}
74-
for i := 1; i <= projectCount; i++ {
75-
names = append(names, fmt.Sprintf("%s%d", projectPrefix, i))
58+
for i := 1; i <= createProjectCount; i++ {
59+
names = append(names, fmt.Sprintf("%s%d", createProjectPrefix, i))
7660
}
7761
}
7862

79-
// create each project via glab API, then clone
80-
createdAny := false
81-
for _, name := range names {
82-
display := strings.TrimSpace(name)
83-
if display == "" {
84-
continue
85-
}
86-
path := slugify(display) // reuse helper from init.go
87-
88-
fmt.Printf("%s Creating project: name=%q path=%q namespace_id=%d\n", icRun, display, path, meta.Group.ID)
89-
create := exec.Command("glab", "api", "-X", "POST", "/projects",
90-
"-f", "name="+display,
91-
"-f", "path="+path,
92-
"-f", "namespace_id="+strconv.FormatInt(meta.Group.ID, 10),
93-
"-f", "visibility=public",
94-
)
95-
out, err := create.Output()
96-
if err != nil {
97-
fmt.Printf("%s create failed for %s: %v\n", icErr, display, err)
98-
continue
99-
}
63+
// 3. EXECUTE
64+
var results []TaskResult
65+
var mu sync.Mutex // Để append kết quả an toàn
10066

101-
// parse response using glProject (already defined in codebase)
102-
var pr glProject
103-
if err := json.Unmarshal(out, &pr); err != nil {
104-
fmt.Printf("%s parse create response failed for %s: %v\n", icErr, display, err)
105-
continue
106-
}
107-
if pr.ID == 0 {
108-
fmt.Printf("%s unexpected response for %s (no ID)\n", icErr, display)
109-
continue
110-
}
111-
fmt.Printf("%s created: id=%d name=%q path=%q\n", icOk, pr.ID, pr.Name, pr.Path)
67+
title := fmt.Sprintf("Creating %d project(s)...", len(names))
11268

113-
// clone locally into folder named by display Name
114-
dest := filepath.Join(wd, display)
115-
url := pr.SSHURLToRepo
116-
if repoProto == "https" {
117-
url = pr.HTTPURLToRepo
118-
}
119-
fmt.Printf("%s cloning → %s\n", icDownload, dest)
120-
clone := exec.Command("git", "clone", "--quiet", url, dest)
121-
if err := clone.Run(); err != nil {
122-
fmt.Printf("%s clone failed: %s (%v)\n", icErr, display, err)
123-
continue
124-
}
125-
fmt.Printf("%s cloned %s\n", icOk, display)
126-
createdAny = true
127-
}
69+
err = RunWithSpinner(title, func() error {
70+
for _, rawName := range names {
71+
display := strings.TrimSpace(rawName)
72+
if display == "" {
73+
continue
74+
}
12875

129-
// refresh subgroup.json (take live snapshot) if at least one created
130-
if createdAny {
131-
fmt.Println("🗂 refreshing .ash/subgroup.json ...")
132-
projects, err := apiListProjects(meta.Group.ID) // lists projects for subgroup
133-
if err != nil {
134-
return err
135-
}
136-
prj := make([]projectIdent, 0, len(projects))
137-
for _, p := range projects {
138-
prj = append(prj, projectIdent{ID: p.ID, Path: p.Path, Name: p.Name})
139-
}
140-
meta.Projects = prj
141-
if err := writeSubgroupJSON(ashDir, meta); err != nil {
142-
return fmt.Errorf("write subgroup.json failed: %w", err)
76+
res := createOneProject(wd, meta.Group.ID, display, createProjectProto)
77+
78+
mu.Lock()
79+
results = append(results, res)
80+
mu.Unlock()
14381
}
144-
fmt.Printf("%s Updated .ash/subgroup.json", icInfo)
145-
}
14682

147-
fmt.Printf("%s Done.", icOk)
83+
// Refresh Metadata Silent
84+
refreshProjectMeta(ashDir, meta.Group.ID)
85+
return nil
86+
})
87+
if err != nil {
88+
return err
89+
}
90+
PrintResults(results)
14891
return nil
14992
},
15093
}
15194

95+
func createOneProject(wd string, groupID int64, name string, proto string) TaskResult {
96+
path := slugify(name)
97+
98+
createCmd := exec.Command("glab", "api", "/projects", "-X", "POST",
99+
"-f", "name="+name,
100+
"-f", "path="+path,
101+
"-f", "namespace_id="+strconv.FormatInt(groupID, 10),
102+
"-f", "visibility=public",
103+
)
104+
105+
out, err := createCmd.Output()
106+
if err != nil {
107+
// Check nếu lỗi do trùng tên hoặc lỗi mạng
108+
return TaskResult{Name: name, Status: "ERR", Message: "GitLab create failed"}
109+
}
110+
111+
var pr glProject
112+
if err := json.Unmarshal(out, &pr); err != nil {
113+
return TaskResult{Name: name, Status: "ERR", Message: "Parse response failed"}
114+
}
115+
116+
// B. Clone
117+
dest := filepath.Join(wd, name)
118+
repoURL := pr.HTTPURLToRepo
119+
if proto == "ssh" {
120+
repoURL = pr.SSHURLToRepo
121+
}
122+
123+
if err := exec.Command("git", "clone", "--quiet", repoURL, dest).Run(); err != nil {
124+
return TaskResult{Name: name, Status: "ERR", Message: "Created but Clone failed"}
125+
}
126+
127+
return TaskResult{Name: name, Status: "OK", Message: "Ready"}
128+
}
129+
130+
func refreshProjectMeta(ashDir string, groupID int64) {
131+
prjs, _ := apiListProjects(groupID)
132+
if len(prjs) > 0 {
133+
var newMeta subgroupMeta
134+
// Đọc lại để giữ thông tin cũ (nếu có field khác), ở đây tui đơn giản hóa
135+
// Nếu file không tồn tại hoặc lỗi đọc thì coi như empty struct
136+
readJSON(filepath.Join(ashDir, "subgroup.json"), &newMeta)
137+
138+
idents := make([]projectIdent, 0, len(prjs))
139+
for _, p := range prjs {
140+
idents = append(idents, projectIdent{ID: p.ID, Path: p.Path, Name: p.Name})
141+
}
142+
newMeta.Projects = idents
143+
// Giữ lại Group ID cũ nếu cần, hoặc giả định apiListProjects là đúng
144+
// Ở đây ta chỉ update list project
145+
writeJSON(filepath.Join(ashDir, "subgroup.json"), newMeta)
146+
}
147+
}
148+
152149
func init() {
153150
projectCmd.AddCommand(projectCreateCmd)
154-
155-
projectCreateCmd.Flags().StringVarP(&projectName, "name", "n", "", "Name of project")
156-
projectCreateCmd.Flags().IntVarP(&projectCount, "count", "c", 0, "Count of project")
157-
projectCreateCmd.Flags().StringVarP(&projectPrefix, "prefix", "p", "", "Create multiple project with Prefix")
158-
projectCreateCmd.Flags().StringVarP(&projectProto, "proto", "g", "https", "Clone project ( https | ssh )")
151+
projectCreateCmd.Flags().IntVarP(&createProjectCount, "count", "c", 0, "Number of projects")
152+
projectCreateCmd.Flags().StringVarP(&createProjectPrefix, "prefix", "p", "", "Prefix for batch creation")
153+
projectCreateCmd.Flags().StringVarP(&createProjectProto, "proto", "g", "https", "Protocol (https/ssh)")
159154
}

cmd/project_sync.go

Lines changed: 71 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,94 @@ package cmd
33
import (
44
"fmt"
55
"os"
6+
"os/exec"
67
"path/filepath"
8+
"sync"
79

810
"github.com/spf13/cobra"
911
)
1012

1113
var projectSyncCmd = &cobra.Command{
12-
Use: "sync",
14+
Use: "sync",
15+
Short: "Sync all projects (Clone/Pull)",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
wd, err := os.Getwd()
18+
if err != nil {
19+
return err
20+
}
21+
subMetaPath := filepath.Join(wd, ".ash", "subgroup.json")
22+
if !fileExists(subMetaPath) {
23+
return fmt.Errorf("not in a subgroup folder")
24+
}
1325

14-
Short: "Sync projects between GitLab and local workspace",
15-
Long: `Synchronize all projects in the current subgroup between GitLab and the local workspace.
26+
var meta subgroupMeta
27+
if err := readJSON(subMetaPath, &meta); err != nil {
28+
return err
29+
}
1630

17-
New projects on GitLab will be cloned automatically.
18-
Deleted projects on GitLab will be removed from subgroup.json.
31+
if len(meta.Projects) == 0 {
32+
fmt.Printf("%s[INFO] No projects found in metadata.%s\n", Cyan, Reset)
33+
return nil
34+
}
1935

20-
Use the --clean flag to also delete local folders corresponding
21-
to projects that no longer exist on GitLab.
36+
// EXECUTE
37+
var results []TaskResult
38+
var mu sync.Mutex
2239

23-
Examples:
24-
ash project sync
25-
→ Updates metadata only (keeps local folders)
26-
ash project sync --clean
27-
→ Deletes local folders for removed projects and updates metadata.`,
28-
SilenceUsage: true,
29-
SilenceErrors: true,
40+
title := fmt.Sprintf("Syncing %d project(s)...", len(meta.Projects))
3041

31-
RunE: func(cmd *cobra.Command, args []string) error {
32-
wd, err := os.Getwd()
42+
err = RunWithSpinner(title, func() error {
43+
var wg sync.WaitGroup
44+
sem := make(chan struct{}, 5) // Limit 5 concurrent threads
45+
46+
for _, p := range meta.Projects {
47+
wg.Add(1)
48+
go func(proj projectIdent) {
49+
defer wg.Done()
50+
sem <- struct{}{}
51+
defer func() { <-sem }()
52+
53+
res := syncOneProject(wd, proj)
54+
55+
mu.Lock()
56+
results = append(results, res)
57+
mu.Unlock()
58+
}(p)
59+
}
60+
wg.Wait()
61+
return nil
62+
})
3363
if err != nil {
34-
return fmt.Errorf("getwd failed: %w", err)
64+
return err
3565
}
36-
ashDir := filepath.Join(wd, ".ash")
37-
// groupMetaPath := filepath.Join(ashDir, "groups.json")
38-
subMetaPath := filepath.Join(ashDir, "subgroups.json")
66+
PrintResults(results)
67+
return nil
68+
},
69+
}
3970

40-
if !fileExists(subMetaPath) {
41-
return fmt.Errorf(RED+"this look like a group folder (found %s); run from the subgroup folder"+RED, subMetaPath)
71+
func syncOneProject(wd string, p projectIdent) TaskResult {
72+
targetDir := filepath.Join(wd, p.Name)
73+
74+
// Case 1: Folder missing -> CLONE
75+
if !fileExists(targetDir) {
76+
// Dùng glab repo clone để nó tự handle auth/url
77+
err := exec.Command("glab", "repo", "clone", p.Path, targetDir).Run()
78+
if err != nil {
79+
return TaskResult{Name: p.Name, Status: "ERR", Message: "Clone failed"}
4280
}
81+
return TaskResult{Name: p.Name, Status: "NEW", Message: "Cloned"}
82+
}
4383

44-
println(GREEN + "Sync called" + GREEN)
84+
// Case 2: Folder exists -> PULL
85+
if !fileExists(filepath.Join(targetDir, ".git")) {
86+
return TaskResult{Name: p.Name, Status: "SKIP", Message: "Not a git repo"}
87+
}
4588

46-
return nil
47-
},
89+
err := exec.Command("git", "-C", targetDir, "pull", "--quiet").Run()
90+
if err != nil {
91+
return TaskResult{Name: p.Name, Status: "ERR", Message: "Pull failed (Conflict?)"}
92+
}
93+
return TaskResult{Name: p.Name, Status: "OK", Message: "Updated"}
4894
}
4995

5096
func init() {

0 commit comments

Comments
 (0)