@@ -2,158 +2,153 @@ package cmd
22
33import (
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
1616var (
17- projectName string
18- projectCount int
19- projectPrefix string
20- projectProto string
17+ createProjectCount int
18+ createProjectPrefix string
19+ createProjectProto string
2120)
2221
2322var 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
3127Examples:
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+
152149func 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}
0 commit comments