You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
330 lines
8.7 KiB
330 lines
8.7 KiB
// 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)
|
|
}
|