blob: 43fbee6a2424fbbb4d8f4895e6f94b7f31b05699 [file] [log] [blame]
// 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)
}