Skip to content

Commit bf60ddc

Browse files
committed
support multiple layouts in one repo
1 parent 6b978e4 commit bf60ddc

13 files changed

Lines changed: 152 additions & 11 deletions

File tree

README.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ however layout offers additional features and bonuses:
4646
- supports multiple inline hooks (with portable shell) and templated hooks
4747
- hooks also supports condition :-)
4848
- supports normal labeling for variables input (cookiecuter...)
49+
- supports multiple layout in one repo
4950

5051
I generally do not like competing with other open-source projects but this time
5152
I would like to say that this project is aiming to fix legacy cookiecutter's problems
@@ -161,11 +162,42 @@ See [configuration](#configuration) for details.
161162

162163
### Layout structure
163164

164-
Each repository should contain:
165+
Once repository fetched, `layout` will scan directories with `layout.yaml` files. Each directory with such file will
166+
be marked as project directory.
167+
168+
In case there is only one project directory, then it will be automatically picked. Otherwise, user will be prompted to
169+
pick desired project (based on `title` field).
170+
171+
Each project directory should contain:
165172

166173
- `layout.yaml` - main manifest file
167174
- `content` - content directory which will be copied to the destination
168175

176+
177+
Valid repo structure:
178+
179+
**one repo - one layout**:
180+
181+
```
182+
/
183+
├── layout.yaml
184+
└── content
185+
```
186+
187+
188+
**one repo - many layouts** (v1.3.0+):
189+
190+
```
191+
/
192+
├── foo
193+
│ └── layoutA
194+
│ ├── layout.yaml
195+
│ └── content
196+
└── layoutB
197+
├── layout.yaml
198+
└── content
199+
```
200+
169201
### Manifest
170202

171203
Check examples in:
@@ -633,7 +665,6 @@ See [roadmap](#roadmap) for planning related features.
633665
- global before/after hooks
634666
- globally disable hooks
635667
- compute variables by script
636-
- multiple templates in one repo
637668
- Delivery
638669
- apt repository
639670
- Arch AUR

internal/deploy.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,31 @@ func Deploy(ctx context.Context, config Config) error {
9696
defer os.RemoveAll(tmpDir)
9797
projectDir = tmpDir
9898
}
99-
manifestFile := filepath.Join(projectDir, ManifestFile)
99+
100+
manifestFiles, err := findManifests(projectDir)
101+
if err != nil {
102+
return fmt.Errorf("find manifests: %w", err)
103+
}
104+
if len(manifestFiles) == 0 {
105+
return fmt.Errorf("no manifests files discovered")
106+
}
107+
108+
var manifestFile string
109+
110+
if len(manifestFiles) == 1 {
111+
// pick first as default
112+
manifestFile = manifestFiles[0]
113+
projectDir = filepath.Dir(manifestFile)
114+
} else {
115+
// ask which manifest to use
116+
selectedManifest, err := selectManifest(ctx, config.Display, manifestFiles)
117+
if err != nil {
118+
return fmt.Errorf("ask for manifest: %w", err)
119+
}
120+
manifestFile = selectedManifest
121+
projectDir = filepath.Dir(selectedManifest)
122+
}
123+
100124
manifest, err := loadManifest(manifestFile)
101125
if err != nil {
102126
return fmt.Errorf("load manifest %s: %w", manifestFile, err)
@@ -116,6 +140,54 @@ func Deploy(ctx context.Context, config Config) error {
116140
return nil
117141
}
118142

143+
func selectManifest(ctx context.Context, display ui.UI, manifests []string) (string, error) {
144+
var options []string
145+
for _, m := range manifests {
146+
manifest, err := loadManifest(m)
147+
if err != nil {
148+
return "", fmt.Errorf("read manifest %s: %w", manifest, err)
149+
}
150+
options = append(options, manifest.Title)
151+
}
152+
picked, err := display.Select(ctx, "Which to use", options[0], options)
153+
if err != nil {
154+
return "", fmt.Errorf("select manifest: %w", err)
155+
}
156+
for i, opt := range options {
157+
if opt == picked {
158+
return manifests[i], nil
159+
}
160+
}
161+
return "", fmt.Errorf("picked unknown manifest")
162+
}
163+
164+
// find manifests in root directory recursive. It will not scan directory with manifest file deeper.
165+
func findManifests(rootDir string) ([]string, error) {
166+
var files []string
167+
err := filepath.Walk(rootDir, func(path string, info fs.FileInfo, err error) error {
168+
if err != nil {
169+
return err
170+
}
171+
if !info.IsDir() {
172+
return nil
173+
}
174+
manifestFile := filepath.Join(path, ManifestFile)
175+
stat, err := os.Stat(manifestFile)
176+
if err != nil {
177+
if os.IsNotExist(err) {
178+
return nil
179+
}
180+
return err
181+
}
182+
if stat.IsDir() {
183+
return nil
184+
}
185+
files = append(files, manifestFile)
186+
return filepath.SkipDir // do not go to layout dir
187+
})
188+
return files, err
189+
}
190+
119191
// clones from git repository into temporary directory.
120192
// Returned directory should be removed by caller.
121193
func cloneFromGit(ctx context.Context, client gitclient.Client, url string) (projectDir string, err error) {

internal/manifest.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ func loadManifest(file string) (*Manifest, error) {
5454
// Communicates with user and renders all templates and executes hooks. Debug flag enables state dump to stdout
5555
// after user input. Once flags disables retry on wrong user input.
5656
func (m *Manifest) renderTo(ctx context.Context, display ui.UI, destinationDir, layoutDir string, debug, once bool, initialState map[string]interface{}) error {
57-
if m.Title != "" {
58-
if err := display.Title(ctx, m.Title); err != nil {
59-
return fmt.Errorf("show title: %w", err)
57+
welcomeMessage := strings.TrimSpace(strings.Join([]string{m.Title, m.Description}, "\n\n"))
58+
if welcomeMessage != "" {
59+
if err := display.Title(ctx, welcomeMessage); err != nil {
60+
return fmt.Errorf("show welcome message: %w", err)
6061
}
6162
}
6263
var state = make(map[string]interface{})

internal/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ const (
2727
)
2828

2929
type Manifest struct {
30-
Version string // minimal layout version (semver). Empty means any version
31-
Title string
32-
Delimiters struct {
30+
Version string // minimal layout version (semver). Empty means any version
31+
Title string // short description of what manifest doing, should be unique in multi-layouts repo
32+
Description string // full manifest description
33+
Delimiters struct {
3334
Open string
3435
Close string
3536
} // custom template delimiter for go templates, default is '{{' and '}}'

layout_test.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func TestRender_basic(t *testing.T) {
4545
defer os.RemoveAll(tempDir)
4646

4747
err = internal.Deploy(context.Background(), internal.Config{
48-
Source: "test-data",
48+
Source: "test-data/projectA",
4949
Target: tempDir,
5050
Display: simple.New(bufio.NewReader(strings.NewReader(
5151
"alice\n1234\n3\nn\n1\n",
@@ -93,7 +93,7 @@ func TestRender_gitClone(t *testing.T) {
9393
w, err := repo.Worktree()
9494
require.NoError(t, err)
9595

96-
files, err := internal.CopyTree("test-data", repoDir)
96+
files, err := internal.CopyTree("test-data/projectA", repoDir)
9797
require.NoError(t, err)
9898

9999
for _, f := range files {
@@ -145,3 +145,38 @@ func TestRender_gitClone(t *testing.T) {
145145
require.NoError(t, err)
146146
require.Equal(t, "Hello world the foo as bar", string(bytes.TrimSpace(content)))
147147
}
148+
149+
func TestRender_multiProject(t *testing.T) {
150+
t.Run("select project A", func(t *testing.T) {
151+
tempDir, err := os.MkdirTemp("", "")
152+
require.NoError(t, err)
153+
defer os.RemoveAll(tempDir)
154+
155+
err = internal.Deploy(context.Background(), internal.Config{
156+
Source: "test-data",
157+
Target: tempDir,
158+
Display: simple.New(bufio.NewReader(strings.NewReader(
159+
"1\nalice\n1234\n3\nn\n1\n",
160+
)), io.Discard),
161+
})
162+
require.NoError(t, err)
163+
assert.FileExists(t, filepath.Join(tempDir, "created.txt"))
164+
165+
})
166+
t.Run("select project B", func(t *testing.T) {
167+
tempDir, err := os.MkdirTemp("", "")
168+
require.NoError(t, err)
169+
defer os.RemoveAll(tempDir)
170+
171+
err = internal.Deploy(context.Background(), internal.Config{
172+
Source: "test-data",
173+
Target: tempDir,
174+
Display: simple.New(bufio.NewReader(strings.NewReader(
175+
"2\n",
176+
)), io.Discard),
177+
})
178+
require.NoError(t, err)
179+
assert.FileExists(t, filepath.Join(tempDir, "layout2"))
180+
181+
})
182+
}
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)