| // Copyright 2020 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package workspace let's you manage workspaces |
| package workspace |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| |
| "android.googlesource.com/platform/tools/treble.git/hacksaw/bind" |
| "android.googlesource.com/platform/tools/treble.git/hacksaw/codebase" |
| "android.googlesource.com/platform/tools/treble.git/hacksaw/config" |
| "android.googlesource.com/platform/tools/treble.git/hacksaw/git" |
| ) |
| |
| type Workspace struct { |
| composer Composer |
| topDir string |
| } |
| |
| func New(bm bind.PathBinder, topDir string) Workspace { |
| return Workspace{NewComposer(bm), topDir} |
| } |
| |
| // Create workspace |
| func (w Workspace) Create(workspaceName string, codebaseName string) (string, error) { |
| cfg := config.GetConfig() |
| _, ok := cfg.Codebases[codebaseName] |
| if !ok { |
| return "", fmt.Errorf("Codebase %s does not exist", codebaseName) |
| } |
| if _, ok := cfg.Workspaces[workspaceName]; ok { |
| return "", fmt.Errorf("Workspace %s already exists", workspaceName) |
| } |
| cfg.Workspaces[workspaceName] = codebaseName |
| workspaceDir, err := w.GetDir(workspaceName) |
| if err != nil { |
| return "", err |
| } |
| if err = os.MkdirAll(workspaceDir, os.ModePerm); err != nil { |
| return "", err |
| } |
| codebaseDir, err := codebase.GetDir(codebaseName) |
| if err != nil { |
| return "", err |
| } |
| //TODO: match the order of parameters with Create |
| if _, err = w.composer.Compose(codebaseDir, workspaceDir); err != nil { |
| return "", err |
| } |
| return workspaceDir, nil |
| } |
| |
| // Recreate workspace |
| func (w Workspace) Recreate(workspaceName string) (string, error) { |
| cfg := config.GetConfig() |
| codebaseName, ok := cfg.Workspaces[workspaceName] |
| if !ok { |
| return "", fmt.Errorf("Workspace %s does not exist", workspaceName) |
| } |
| workspaceDir, err := w.GetDir(workspaceName) |
| if err != nil { |
| return "", err |
| } |
| codebaseDir, err := codebase.GetDir(codebaseName) |
| if err != nil { |
| return "", err |
| } |
| if _, err = w.composer.Compose(codebaseDir, workspaceDir); err != nil { |
| return "", err |
| } |
| return workspaceDir, nil |
| } |
| |
| // GetDir retrieves the directory of a specific workspace |
| func (w Workspace) GetDir(workspaceName string) (string, error) { |
| cfg := config.GetConfig() |
| _, ok := cfg.Workspaces[workspaceName] |
| if !ok { |
| return "", fmt.Errorf("Workspace %s not found", workspaceName) |
| } |
| dir := filepath.Join(w.topDir, workspaceName) |
| return dir, nil |
| } |
| |
| // GetCodebase retrieves the codebase that a workspace belongs to |
| func (w Workspace) GetCodebase(workspaceName string) (string, error) { |
| cfg := config.GetConfig() |
| codebase, ok := cfg.Workspaces[workspaceName] |
| if !ok { |
| return "", fmt.Errorf("Workspace %s not found", workspaceName) |
| } |
| return codebase, nil |
| } |
| |
| //SetTopDir sets the directory that contains all workspaces |
| func (w *Workspace) SetTopDir(dir string) { |
| w.topDir = dir |
| } |
| |
| func (w Workspace) List() map[string]string { |
| cfg := config.GetConfig() |
| list := make(map[string]string) |
| for name, codebaseName := range cfg.Workspaces { |
| list[name] = codebaseName |
| } |
| return list |
| } |
| |
| func (w Workspace) DetachGitWorktrees(workspaceName string, unbindList []string) error { |
| workspaceDir, err := w.GetDir(workspaceName) |
| if err != nil { |
| return err |
| } |
| workspaceDir, err = filepath.Abs(workspaceDir) |
| if err != nil { |
| return err |
| } |
| //resolve all symlinks so it can be |
| //matched to mount paths |
| workspaceDir, err = filepath.EvalSymlinks(workspaceDir) |
| if err != nil { |
| return err |
| } |
| codebaseName, err := w.GetCodebase(workspaceName) |
| if err != nil { |
| return err |
| } |
| codebaseDir, err := codebase.GetDir(codebaseName) |
| if err != nil { |
| return err |
| } |
| lister := git.NewRepoLister() |
| gitProjects, err := lister.List(codebaseDir) |
| if err != nil { |
| return err |
| } |
| gitWorktrees := make(map[string]bool) |
| for _, project := range gitProjects { |
| gitWorktrees[project] = true |
| } |
| //projects that were unbound were definitely |
| //never git worktrees |
| for _, unbindPath := range unbindList { |
| project, err := filepath.Rel(workspaceDir, unbindPath) |
| if err != nil { |
| return err |
| } |
| if _, ok := gitWorktrees[project]; ok { |
| gitWorktrees[project] = false |
| } |
| } |
| for project, isWorktree := range gitWorktrees { |
| if !isWorktree { |
| continue |
| } |
| codebaseProject := filepath.Join(codebaseDir, project) |
| workspaceProject := filepath.Join(workspaceDir, project) |
| _, err = os.Stat(workspaceProject) |
| if err == nil { |
| //proceed to detach |
| } else if os.IsNotExist(err) { |
| //just skip if it doesn't exist |
| continue |
| } else { |
| return err |
| } |
| contents, err := ioutil.ReadDir(workspaceProject) |
| if err != nil { |
| return err |
| } |
| if len(contents) == 0 { |
| //empty directory, not even a .git |
| //not a wortree |
| continue |
| } |
| fmt.Print(".") |
| cmd := exec.Command("git", |
| "-C", codebaseProject, |
| "worktree", "remove", "--force", workspaceProject) |
| output, err := cmd.CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", |
| cmd.String(), err.Error(), output) |
| } |
| cmd = exec.Command("git", |
| "-C", codebaseProject, |
| "branch", "--delete", "--force", workspaceName) |
| output, err = cmd.CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", |
| cmd.String(), err.Error(), output) |
| } |
| } |
| return nil |
| } |
| |
| func (w Workspace) Remove(remove string) (*config.Config, error) { |
| cfg := config.GetConfig() |
| _, ok := cfg.Workspaces[remove] |
| if !ok { |
| return cfg, fmt.Errorf("Workspace %s not found", remove) |
| } |
| workspaceDir, err := w.GetDir(remove) |
| if err != nil { |
| return cfg, err |
| } |
| unbindList, err := w.composer.Dismantle(workspaceDir) |
| if err != nil { |
| return cfg, err |
| } |
| fmt.Print("Detaching worktrees") |
| if err = w.DetachGitWorktrees(remove, unbindList); err != nil { |
| return cfg, err |
| } |
| fmt.Print("\n") |
| fmt.Println("Removing files") |
| if err = os.RemoveAll(workspaceDir); err != nil { |
| return cfg, err |
| } |
| delete(cfg.Workspaces, remove) |
| return cfg, err |
| } |
| |
| func (w Workspace) Edit(editPath string) (string, string, error) { |
| editPath, err := filepath.Abs(editPath) |
| if err != nil { |
| return "", "", err |
| } |
| editPath, err = filepath.EvalSymlinks(editPath) |
| if err != nil { |
| return "", "", err |
| } |
| relProjectPath, err := w.getReadOnlyProjectFromPath(editPath) |
| if err != nil { |
| return "", "", err |
| } |
| workspaceName, err := w.getWorkspaceFromPath(editPath) |
| if err != nil { |
| return "", "", err |
| } |
| workspaceDir, err := w.GetDir(workspaceName) |
| if err != nil { |
| return "", "", err |
| } |
| codebaseName, err := w.GetCodebase(workspaceName) |
| if err != nil { |
| return "", "", err |
| } |
| codebaseDir, err := codebase.GetDir(codebaseName) |
| if err != nil { |
| return "", "", err |
| } |
| wsProjectPath := filepath.Join(workspaceDir, relProjectPath) |
| if err = w.composer.Unbind(wsProjectPath); err != nil { |
| return "", "", err |
| } |
| //TODO: support editing nested projects |
| //the command above unbinds nested child projects but |
| //we don't rebind them after checking out an editable project branch |
| cbProjectPath := filepath.Join(codebaseDir, relProjectPath) |
| branchName := workspaceName |
| cmd := exec.Command("git", |
| "-C", cbProjectPath, |
| "worktree", "add", |
| "-b", branchName, |
| wsProjectPath) |
| output, err := cmd.CombinedOutput() |
| if err != nil { |
| return "", "", fmt.Errorf("Command\n%s\nfailed with the following:\n%s\n%s", |
| cmd.String(), err.Error(), output) |
| } |
| return branchName, wsProjectPath, err |
| } |
| |
| func (w Workspace) getReadOnlyProjectFromPath(inPath string) (string, error) { |
| worspaceName, err := w.getWorkspaceFromPath(inPath) |
| if err != nil { |
| return "", err |
| } |
| workspacePath, err := w.GetDir(worspaceName) |
| if err != nil { |
| return "", err |
| } |
| bindList, err := w.composer.List(workspacePath) |
| if err != nil { |
| return "", err |
| } |
| for _, bindPath := range bindList { |
| if !strings.HasPrefix(inPath+"/", bindPath+"/") { |
| continue |
| } |
| relProjectPath, err := filepath.Rel(workspacePath, bindPath) |
| if err != nil { |
| return "", err |
| } |
| return relProjectPath, nil |
| } |
| return "", fmt.Errorf("Path %s is already editable", inPath) |
| } |
| |
| func (w Workspace) getWorkspaceFromPath(inPath string) (string, error) { |
| for workspaceName, _ := range w.List() { |
| dir, err := w.GetDir(workspaceName) |
| if err != nil { |
| return "", err |
| } |
| if strings.HasPrefix(inPath+"/", dir+"/") { |
| return workspaceName, nil |
| } |
| } |
| return "", fmt.Errorf("Path %s is not contained in a workspace", inPath) |
| } |