• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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