1// Copyright 2020 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// Package workspace let's you manage workspaces 16package workspace 17 18import ( 19 "fmt" 20 "io/ioutil" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "strings" 25 26 "android.googlesource.com/platform/tools/treble.git/hacksaw/bind" 27 "android.googlesource.com/platform/tools/treble.git/hacksaw/codebase" 28 "android.googlesource.com/platform/tools/treble.git/hacksaw/config" 29 "android.googlesource.com/platform/tools/treble.git/hacksaw/git" 30) 31 32type Workspace struct { 33 composer Composer 34 topDir string 35} 36 37func New(bm bind.PathBinder, topDir string) Workspace { 38 return Workspace{NewComposer(bm), topDir} 39} 40 41// Create workspace 42func (w Workspace) Create(workspaceName string, codebaseName string) (string, error) { 43 cfg := config.GetConfig() 44 _, ok := cfg.Codebases[codebaseName] 45 if !ok { 46 return "", fmt.Errorf("Codebase %s does not exist", codebaseName) 47 } 48 if _, ok := cfg.Workspaces[workspaceName]; ok { 49 return "", fmt.Errorf("Workspace %s already exists", workspaceName) 50 } 51 cfg.Workspaces[workspaceName] = codebaseName 52 workspaceDir, err := w.GetDir(workspaceName) 53 if err != nil { 54 return "", err 55 } 56 if err = os.MkdirAll(workspaceDir, os.ModePerm); err != nil { 57 return "", err 58 } 59 codebaseDir, err := codebase.GetDir(codebaseName) 60 if err != nil { 61 return "", err 62 } 63 //TODO: match the order of parameters with Create 64 if _, err = w.composer.Compose(codebaseDir, workspaceDir); err != nil { 65 return "", err 66 } 67 return workspaceDir, nil 68} 69 70// Recreate workspace 71func (w Workspace) Recreate(workspaceName string) (string, error) { 72 cfg := config.GetConfig() 73 codebaseName, ok := cfg.Workspaces[workspaceName] 74 if !ok { 75 return "", fmt.Errorf("Workspace %s does not exist", workspaceName) 76 } 77 workspaceDir, err := w.GetDir(workspaceName) 78 if err != nil { 79 return "", err 80 } 81 codebaseDir, err := codebase.GetDir(codebaseName) 82 if err != nil { 83 return "", err 84 } 85 if _, err = w.composer.Compose(codebaseDir, workspaceDir); err != nil { 86 return "", err 87 } 88 return workspaceDir, nil 89} 90 91// GetDir retrieves the directory of a specific workspace 92func (w Workspace) GetDir(workspaceName string) (string, error) { 93 cfg := config.GetConfig() 94 _, ok := cfg.Workspaces[workspaceName] 95 if !ok { 96 return "", fmt.Errorf("Workspace %s not found", workspaceName) 97 } 98 dir := filepath.Join(w.topDir, workspaceName) 99 return dir, nil 100} 101 102// GetCodebase retrieves the codebase that a workspace belongs to 103func (w Workspace) GetCodebase(workspaceName string) (string, error) { 104 cfg := config.GetConfig() 105 codebase, ok := cfg.Workspaces[workspaceName] 106 if !ok { 107 return "", fmt.Errorf("Workspace %s not found", workspaceName) 108 } 109 return codebase, nil 110} 111 112//SetTopDir sets the directory that contains all workspaces 113func (w *Workspace) SetTopDir(dir string) { 114 w.topDir = dir 115} 116 117func (w Workspace) List() map[string]string { 118 cfg := config.GetConfig() 119 list := make(map[string]string) 120 for name, codebaseName := range cfg.Workspaces { 121 list[name] = codebaseName 122 } 123 return list 124} 125 126func (w Workspace) DetachGitWorktrees(workspaceName string, unbindList []string) error { 127 workspaceDir, err := w.GetDir(workspaceName) 128 if err != nil { 129 return err 130 } 131 workspaceDir, err = filepath.Abs(workspaceDir) 132 if err != nil { 133 return err 134 } 135 //resolve all symlinks so it can be 136 //matched to mount paths 137 workspaceDir, err = filepath.EvalSymlinks(workspaceDir) 138 if err != nil { 139 return err 140 } 141 codebaseName, err := w.GetCodebase(workspaceName) 142 if err != nil { 143 return err 144 } 145 codebaseDir, err := codebase.GetDir(codebaseName) 146 if err != nil { 147 return err 148 } 149 lister := git.NewRepoLister() 150 gitProjects, err := lister.List(codebaseDir) 151 if err != nil { 152 return err 153 } 154 gitWorktrees := make(map[string]bool) 155 for _, project := range gitProjects { 156 gitWorktrees[project] = true 157 } 158 //projects that were unbound were definitely 159 //never git worktrees 160 for _, unbindPath := range unbindList { 161 project, err := filepath.Rel(workspaceDir, unbindPath) 162 if err != nil { 163 return err 164 } 165 if _, ok := gitWorktrees[project]; ok { 166 gitWorktrees[project] = false 167 } 168 } 169 for project, isWorktree := range gitWorktrees { 170 if !isWorktree { 171 continue 172 } 173 codebaseProject := filepath.Join(codebaseDir, project) 174 workspaceProject := filepath.Join(workspaceDir, project) 175 _, err = os.Stat(workspaceProject) 176 if err == nil { 177 //proceed to detach 178 } else if os.IsNotExist(err) { 179 //just skip if it doesn't exist 180 continue 181 } else { 182 return err 183 } 184 contents, err := ioutil.ReadDir(workspaceProject) 185 if err != nil { 186 return err 187 } 188 if len(contents) == 0 { 189 //empty directory, not even a .git 190 //not a wortree 191 continue 192 } 193 fmt.Print(".") 194 cmd := exec.Command("git", 195 "-C", codebaseProject, 196 "worktree", "remove", "--force", workspaceProject) 197 output, err := cmd.CombinedOutput() 198 if err != nil { 199 return fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", 200 cmd.String(), err.Error(), output) 201 } 202 cmd = exec.Command("git", 203 "-C", codebaseProject, 204 "branch", "--delete", "--force", workspaceName) 205 output, err = cmd.CombinedOutput() 206 if err != nil { 207 return fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", 208 cmd.String(), err.Error(), output) 209 } 210 } 211 return nil 212} 213 214func (w Workspace) Remove(remove string) (*config.Config, error) { 215 cfg := config.GetConfig() 216 _, ok := cfg.Workspaces[remove] 217 if !ok { 218 return cfg, fmt.Errorf("Workspace %s not found", remove) 219 } 220 workspaceDir, err := w.GetDir(remove) 221 if err != nil { 222 return cfg, err 223 } 224 unbindList, err := w.composer.Dismantle(workspaceDir) 225 if err != nil { 226 return cfg, err 227 } 228 fmt.Print("Detaching worktrees") 229 if err = w.DetachGitWorktrees(remove, unbindList); err != nil { 230 return cfg, err 231 } 232 fmt.Print("\n") 233 fmt.Println("Removing files") 234 if err = os.RemoveAll(workspaceDir); err != nil { 235 return cfg, err 236 } 237 delete(cfg.Workspaces, remove) 238 return cfg, err 239} 240 241func (w Workspace) Edit(editPath string) (string, string, error) { 242 editPath, err := filepath.Abs(editPath) 243 if err != nil { 244 return "", "", err 245 } 246 editPath, err = filepath.EvalSymlinks(editPath) 247 if err != nil { 248 return "", "", err 249 } 250 relProjectPath, err := w.getReadOnlyProjectFromPath(editPath) 251 if err != nil { 252 return "", "", err 253 } 254 workspaceName, err := w.getWorkspaceFromPath(editPath) 255 if err != nil { 256 return "", "", err 257 } 258 workspaceDir, err := w.GetDir(workspaceName) 259 if err != nil { 260 return "", "", err 261 } 262 codebaseName, err := w.GetCodebase(workspaceName) 263 if err != nil { 264 return "", "", err 265 } 266 codebaseDir, err := codebase.GetDir(codebaseName) 267 if err != nil { 268 return "", "", err 269 } 270 wsProjectPath := filepath.Join(workspaceDir, relProjectPath) 271 if err = w.composer.Unbind(wsProjectPath); err != nil { 272 return "", "", err 273 } 274 //TODO: support editing nested projects 275 //the command above unbinds nested child projects but 276 //we don't rebind them after checking out an editable project branch 277 cbProjectPath := filepath.Join(codebaseDir, relProjectPath) 278 branchName := workspaceName 279 cmd := exec.Command("git", 280 "-C", cbProjectPath, 281 "worktree", "add", 282 "-b", branchName, 283 wsProjectPath) 284 output, err := cmd.CombinedOutput() 285 if err != nil { 286 return "", "", fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", 287 cmd.String(), err.Error(), output) 288 } 289 return branchName, wsProjectPath, err 290} 291 292func (w Workspace) getReadOnlyProjectFromPath(inPath string) (string, error) { 293 worspaceName, err := w.getWorkspaceFromPath(inPath) 294 if err != nil { 295 return "", err 296 } 297 workspacePath, err := w.GetDir(worspaceName) 298 if err != nil { 299 return "", err 300 } 301 bindList, err := w.composer.List(workspacePath) 302 if err != nil { 303 return "", err 304 } 305 for _, bindPath := range bindList { 306 if !strings.HasPrefix(inPath+"/", bindPath+"/") { 307 continue 308 } 309 relProjectPath, err := filepath.Rel(workspacePath, bindPath) 310 if err != nil { 311 return "", err 312 } 313 return relProjectPath, nil 314 } 315 return "", fmt.Errorf("Path %s is already editable", inPath) 316} 317 318func (w Workspace) getWorkspaceFromPath(inPath string) (string, error) { 319 for workspaceName, _ := range w.List() { 320 dir, err := w.GetDir(workspaceName) 321 if err != nil { 322 return "", err 323 } 324 if strings.HasPrefix(inPath+"/", dir+"/") { 325 return workspaceName, nil 326 } 327 } 328 return "", fmt.Errorf("Path %s is not contained in a workspace", inPath) 329} 330