@@ -14,6 +14,7 @@ import (
1414
1515 "github.com/flowexec/flow/cmd/internal/flags"
1616 workspaceIO "github.com/flowexec/flow/internal/io/workspace"
17+ "github.com/flowexec/flow/internal/services/git"
1718 "github.com/flowexec/flow/pkg/cache"
1819 "github.com/flowexec/flow/pkg/context"
1920 "github.com/flowexec/flow/pkg/filesystem"
@@ -30,6 +31,7 @@ func RegisterWorkspaceCmd(ctx *context.Context, rootCmd *cobra.Command) {
3031 Short : "Manage development workspaces." ,
3132 }
3233 registerAddWorkspaceCmd (ctx , wsCmd )
34+ registerUpdateWorkspaceCmd (ctx , wsCmd )
3335 registerSwitchWorkspaceCmd (ctx , wsCmd )
3436 registerRemoveWorkspaceCmd (ctx , wsCmd )
3537 registerListWorkspaceCmd (ctx , wsCmd )
@@ -39,78 +41,221 @@ func RegisterWorkspaceCmd(ctx *context.Context, rootCmd *cobra.Command) {
3941
4042func registerAddWorkspaceCmd (ctx * context.Context , wsCmd * cobra.Command ) {
4143 createCmd := & cobra.Command {
42- Use : "add NAME PATH " ,
44+ Use : "add NAME PATH_OR_GIT_URL " ,
4345 Aliases : []string {"init" , "create" , "new" },
44- Short : "Initialize a new workspace." ,
45- Args : cobra .ExactArgs (2 ),
46- Run : func (cmd * cobra.Command , args []string ) { addWorkspaceFunc (ctx , cmd , args ) },
46+ Short : "Initialize a new workspace from a local path or Git URL." ,
47+ Long : "Initialize a new workspace. PATH_OR_GIT_URL can be a local directory path " +
48+ "or a Git repository URL (HTTPS or SSH). When a Git URL is provided, " +
49+ "the repository is cloned to the flow cache directory and registered as a workspace.\n \n " +
50+ "Examples:\n " +
51+ " flow workspace add my-ws ./path/to/dir\n " +
52+ " flow workspace add shared https://github.com/org/flows.git\n " +
53+ " flow workspace add tools git@github.com:org/tools.git --branch main\n " +
54+ " flow workspace add stable https://github.com/org/flows.git --tag v1.0.0" ,
55+ Args : cobra .ExactArgs (2 ),
56+ Run : func (cmd * cobra.Command , args []string ) { addWorkspaceFunc (ctx , cmd , args ) },
4757 }
4858 RegisterFlag (ctx , createCmd , * flags .SetAfterCreateFlag )
59+ RegisterFlag (ctx , createCmd , * flags .GitBranchFlag )
60+ RegisterFlag (ctx , createCmd , * flags .GitTagFlag )
4961 wsCmd .AddCommand (createCmd )
5062}
5163
5264func addWorkspaceFunc (ctx * context.Context , cmd * cobra.Command , args []string ) {
5365 name := args [0 ]
54- path := args [1 ]
66+ pathOrURL := args [1 ]
5567
5668 userConfig := ctx .Config
5769 if _ , found := userConfig .Workspaces [name ]; found {
5870 logger .Log ().Fatalf ("workspace %s already exists at %s" , name , userConfig .Workspaces [name ])
5971 }
6072
73+ branch := flags .ValueFor [string ](cmd , * flags .GitBranchFlag , false )
74+ tag := flags .ValueFor [string ](cmd , * flags .GitTagFlag , false )
75+ if branch != "" && tag != "" {
76+ logger .Log ().Fatalf ("cannot specify both --branch and --tag" )
77+ }
78+
79+ var path string
80+ if git .IsGitURL (pathOrURL ) {
81+ path = cloneGitWorkspace (name , pathOrURL , branch , tag )
82+ } else {
83+ path = initLocalWorkspace (name , pathOrURL , branch , tag )
84+ }
85+
86+ userConfig .Workspaces [name ] = path
87+
88+ set := flags .ValueFor [bool ](cmd , * flags .SetAfterCreateFlag , false )
89+ if set {
90+ userConfig .CurrentWorkspace = name
91+ logger .Log ().Infof ("Workspace '%s' set as current workspace" , name )
92+ }
93+
94+ if err := filesystem .WriteConfig (userConfig ); err != nil {
95+ logger .Log ().FatalErr (err )
96+ }
97+
98+ if err := cache .UpdateAll (ctx .DataStore ); err != nil {
99+ logger .Log ().FatalErr (errors .Wrap (err , "failure updating cache" ))
100+ }
101+
102+ logger .Log ().PlainTextSuccess (fmt .Sprintf ("Workspace '%s' created in %s" , name , path ))
103+ }
104+
105+ func initLocalWorkspace (name , pathOrURL , branch , tag string ) string {
106+ if branch != "" || tag != "" {
107+ logger .Log ().Fatalf ("--branch and --tag flags are only supported with Git URLs" )
108+ }
109+ path := resolveLocalPath (pathOrURL , name )
110+ if ! filesystem .WorkspaceConfigExists (path ) {
111+ if err := filesystem .InitWorkspaceConfig (name , path ); err != nil {
112+ logger .Log ().FatalErr (err )
113+ }
114+ }
115+ return path
116+ }
117+
118+ func cloneGitWorkspace (name , gitURL , branch , tag string ) string {
119+ clonePath , err := git .ClonePath (gitURL )
120+ if err != nil {
121+ logger .Log ().FatalErr (errors .Wrap (err , "unable to determine clone path" ))
122+ }
123+
124+ logger .Log ().Infof ("Cloning %s..." , gitURL )
125+ if err := git .Clone (gitURL , clonePath , branch , tag ); err != nil {
126+ logger .Log ().FatalErr (errors .Wrap (err , "unable to clone git repository" ))
127+ }
128+
129+ var wsCfg * workspace.Workspace
130+ if filesystem .WorkspaceConfigExists (clonePath ) {
131+ wsCfg , err = filesystem .LoadWorkspaceConfig (name , clonePath )
132+ if err != nil {
133+ logger .Log ().FatalErr (errors .Wrap (err , "unable to load cloned workspace config" ))
134+ }
135+ } else {
136+ wsCfg = workspace .DefaultWorkspaceConfig (name )
137+ }
138+
139+ wsCfg .GitRemote = gitURL
140+ if branch != "" {
141+ wsCfg .GitRef = branch
142+ wsCfg .GitRefType = workspace .WorkspaceGitRefTypeBranch
143+ } else if tag != "" {
144+ wsCfg .GitRef = tag
145+ wsCfg .GitRefType = workspace .WorkspaceGitRefTypeTag
146+ }
147+
148+ if err := filesystem .WriteWorkspaceConfig (clonePath , wsCfg ); err != nil {
149+ logger .Log ().FatalErr (errors .Wrap (err , "unable to write workspace config with git metadata" ))
150+ }
151+ return clonePath
152+ }
153+
154+ func resolveLocalPath (path , name string ) string {
61155 switch {
62156 case path == "" :
63- path = filepath .Join (filesystem .CachedDataDirPath (), name )
157+ return filepath .Join (filesystem .CachedDataDirPath (), name )
64158 case path == "." || strings .HasPrefix (path , "./" ):
65159 wd , err := os .Getwd ()
66160 if err != nil {
67161 logger .Log ().FatalErr (err )
68162 }
69163 if path == "." {
70- path = wd
71- } else {
72- path = fmt .Sprintf ("%s/%s" , wd , path [2 :])
164+ return wd
73165 }
166+ return fmt .Sprintf ("%s/%s" , wd , path [2 :])
74167 case path == "~" || strings .HasPrefix (path , "~/" ):
75168 hd , err := os .UserHomeDir ()
76169 if err != nil {
77170 logger .Log ().FatalErr (err )
78171 }
79172 if path == "~" {
80- path = hd
81- } else {
82- path = fmt .Sprintf ("%s/%s" , hd , path [2 :])
173+ return hd
83174 }
175+ return fmt .Sprintf ("%s/%s" , hd , path [2 :])
84176 case ! filepath .IsAbs (path ):
85177 wd , err := os .Getwd ()
86178 if err != nil {
87179 logger .Log ().FatalErr (err )
88180 }
89- path = fmt .Sprintf ("%s/%s" , wd , path )
181+ return fmt .Sprintf ("%s/%s" , wd , path )
182+ default :
183+ return path
90184 }
185+ }
91186
92- if ! filesystem .WorkspaceConfigExists (path ) {
93- if err := filesystem .InitWorkspaceConfig (name , path ); err != nil {
94- logger .Log ().FatalErr (err )
187+ func registerUpdateWorkspaceCmd (ctx * context.Context , wsCmd * cobra.Command ) {
188+ updateCmd := & cobra.Command {
189+ Use : "update [NAME]" ,
190+ Aliases : []string {"pull" , "sync" },
191+ Short : "Pull latest changes for a git-sourced workspace." ,
192+ Long : "Pull the latest changes from the git remote for a workspace that was added from a Git URL. " +
193+ "If NAME is omitted, the current workspace is used.\n \n " +
194+ "This respects the branch or tag that was originally specified when the workspace was added.\n " +
195+ "Use --force to discard local changes and hard reset to the remote." ,
196+ Args : cobra .MaximumNArgs (1 ),
197+ ValidArgsFunction : func (_ * cobra.Command , _ []string , _ string ) ([]cobra.Completion , cobra.ShellCompDirective ) {
198+ return maps .Keys (ctx .Config .Workspaces ), cobra .ShellCompDirectiveNoFileComp
199+ },
200+ Run : func (cmd * cobra.Command , args []string ) { updateWorkspaceFunc (ctx , cmd , args ) },
201+ }
202+ RegisterFlag (ctx , updateCmd , * flags .ForceFlag )
203+ wsCmd .AddCommand (updateCmd )
204+ }
205+
206+ func updateWorkspaceFunc (ctx * context.Context , cmd * cobra.Command , args []string ) {
207+ var workspaceName , wsPath string
208+ if len (args ) == 1 {
209+ workspaceName = args [0 ]
210+ wsPath = ctx .Config .Workspaces [workspaceName ]
211+ if wsPath == "" {
212+ logger .Log ().Fatalf ("workspace %s not found" , workspaceName )
95213 }
214+ } else {
215+ if ctx .CurrentWorkspace == nil {
216+ logger .Log ().Fatalf ("no current workspace set" )
217+ }
218+ workspaceName = ctx .CurrentWorkspace .AssignedName ()
219+ wsPath = ctx .CurrentWorkspace .Location ()
96220 }
97- userConfig .Workspaces [name ] = path
98221
99- set := flags .ValueFor [bool ](cmd , * flags .SetAfterCreateFlag , false )
100- if set {
101- userConfig .CurrentWorkspace = name
102- logger .Log ().Infof ("Workspace '%s' set as current workspace" , name )
222+ force := flags .ValueFor [bool ](cmd , * flags .ForceFlag , false )
223+
224+ wsCfg , err := filesystem .LoadWorkspaceConfig (workspaceName , wsPath )
225+ if err != nil {
226+ logger .Log ().FatalErr (errors .Wrap (err , "unable to load workspace config" ))
103227 }
104228
105- if err := filesystem .WriteConfig (userConfig ); err != nil {
106- logger .Log ().FatalErr (err )
229+ if wsCfg .GitRemote == "" {
230+ logger .Log ().Fatalf ("workspace '%s' is not a git-sourced workspace (no gitRemote set in flow.yaml)" , workspaceName )
231+ }
232+
233+ if force {
234+ logger .Log ().Warnf (
235+ "Force updating workspace '%s' from %s (local changes will be discarded)..." ,
236+ workspaceName , wsCfg .GitRemote ,
237+ )
238+ } else {
239+ logger .Log ().Infof ("Updating workspace '%s' from %s..." , workspaceName , wsCfg .GitRemote )
240+ }
241+
242+ if force {
243+ err = git .ResetPull (wsPath , wsCfg .GitRef , string (wsCfg .GitRefType ))
244+ } else {
245+ err = git .Pull (wsPath , wsCfg .GitRef , string (wsCfg .GitRefType ))
246+ }
247+ if err != nil {
248+ if ! force {
249+ logger .Log ().Warnf ("Hint: use --force to discard local changes and hard reset to remote" )
250+ }
251+ logger .Log ().FatalErr (errors .Wrap (err , "unable to update workspace" ))
107252 }
108253
109254 if err := cache .UpdateAll (ctx .DataStore ); err != nil {
110255 logger .Log ().FatalErr (errors .Wrap (err , "failure updating cache" ))
111256 }
112257
113- logger .Log ().PlainTextSuccess (fmt .Sprintf ("Workspace '%s' created in %s " , name , path ))
258+ logger .Log ().PlainTextSuccess (fmt .Sprintf ("Workspace '%s' updated " , workspaceName ))
114259}
115260
116261func registerSwitchWorkspaceCmd (ctx * context.Context , setCmd * cobra.Command ) {
0 commit comments