Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ LDFLAGS = -s -w \
-X $(PKG).Commit=$(COMMIT) \
-X $(PKG).Date=$(DATE)

.PHONY: build build-windows build-tray build-ui install-ui-deps test-ui install install-installer licenses test clean release release-snapshot
.PHONY: build build-windows build-tray build-ui install-ui-deps test-ui install install-installer licenses test dupes clean release release-snapshot

UI_INSTALL_STAMP = $(UI_DIR)/node_modules/.package-lock.json

Expand Down Expand Up @@ -97,6 +97,18 @@ test-installer:

test-all: test test-ui test-installer

# Copy-paste report like PhpStorm's Duplicated Code, opened in the browser.
# Pinned so the scan is reproducible; tests are left out because table-driven
# setup repeats on purpose.
dupes:
npx --yes jscpd@5.3.3 internal cmd \
--format go,typescript,javascript,markup \
--formats-exts markup:svelte \
--ignore '**/*_test.go,**/*.test.ts,**/node_modules/**,**/dist/**,**/demo/**,**/paraglide/**' \
--min-tokens 70 --reporters html,json,console --silent \
--output $(BUILD_DIR)/dupes
xdg-open $(BUILD_DIR)/dupes/jscpd-report.html >/dev/null 2>&1 || true

clean:
rm -rf $(BUILD_DIR)
rm -rf $(UI_DIR)/dist
Expand Down
1 change: 1 addition & 0 deletions docs/contributing/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ make install # build + install to ~/.local/bin/lerd
make test # go test ./...
make test-ui # run Vitest suite for the web UI
make test-all # test + test-ui + test-installer (bats)
make dupes # copy-paste report for Go, TS and Svelte, opened in the browser
make clean # remove ./build/ and internal/ui/web/dist/
```

Expand Down
4 changes: 4 additions & 0 deletions docs/contributing/pull-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ make test
CGO_ENABLED=1 go vet ./...
```

### Duplication

`make dupes` runs jscpd over `internal/` and `cmd/` and opens an HTML report of copy-pasted blocks, like PhpStorm's Duplicated Code inspection. It is not a CI gate; run it before extracting a helper to find every copy, and after, to check none are left. Tests are left out because table-driven setup repeats on purpose.

### Screenshots for UI changes

If your PR changes anything you can see, in the web UI, the TUI or the tray, add screenshots to the PR description. Show the affected view before and after the change, and include both light and dark themes when the change touches colours or layout. A short screen recording works better than screenshots for animations or multi-step flows.
Expand Down
30 changes: 12 additions & 18 deletions internal/cli/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ func resolveDBLenient(cwd, flagService, flagDatabase string) (*dbEnv, error) {
return env, nil
}

func loadDBEnv(cwd string) (*dbEnv, error) {
// readDBEnvValues reads the site's .env into a plain key/value map, quotes
// stripped, for working out which database it talks to.
func readDBEnvValues(cwd string) (map[string]string, error) {
envPath := filepath.Join(cwd, ".env")
f, err := os.Open(envPath)
if err != nil {
Expand All @@ -240,6 +242,14 @@ func loadDBEnv(cwd string) (*dbEnv, error) {
if err := scanner.Err(); err != nil {
return nil, err
}
return vals, nil
}

func loadDBEnv(cwd string) (*dbEnv, error) {
vals, err := readDBEnvValues(cwd)
if err != nil {
return nil, err
}

urlEnv := parseDBURL(vals["DATABASE_URL"])

Expand Down Expand Up @@ -751,24 +761,8 @@ func parseDBURL(rawURL string) *dbEnv {

// loadDBEnvLenient reads DB connection info from .env without requiring DB_DATABASE.
func loadDBEnvLenient(cwd string) (*dbEnv, error) {
envPath := filepath.Join(cwd, ".env")
f, err := os.Open(envPath)
vals, err := readDBEnvValues(cwd)
if err != nil {
return nil, fmt.Errorf("no .env found in %s", cwd)
}
defer f.Close()

vals := map[string]string{}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
continue
}
k, v, _ := strings.Cut(line, "=")
vals[strings.TrimSpace(k)] = strings.Trim(strings.TrimSpace(v), `"'`)
}
if err := scanner.Err(); err != nil {
return nil, err
}

Expand Down
36 changes: 20 additions & 16 deletions internal/cli/db_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,10 @@ func runDbSnapshot(name, service, database string, allDatabases bool) error {
if err != nil {
return err
}
env, err := resolveDBForSnapshot(cwd, service, database, allDatabases)
env, target, err := snapshotTargetIn(cwd, service, database, allDatabases, "snapshot")
if err != nil {
return err
}
target := snapshotTarget(env, allDatabases)
if !serviceops.SnapshotSupported(target.Service, target.AllDatabases) {
return fmt.Errorf("the %q service does not declare database snapshots", env.service)
}
if !target.AllDatabases && target.Database == "" {
return fmt.Errorf("no database resolved — pass --database, or --all-databases to snapshot the whole service")
}
if err := ensureServiceRunning(env.service); err != nil {
return fmt.Errorf("could not start %s: %w", env.service, err)
}
Expand Down Expand Up @@ -167,17 +160,10 @@ func runDbRestore(name, service, database string, allDatabases, force bool) erro
if err != nil {
return err
}
env, err := resolveDBForSnapshot(cwd, service, database, allDatabases)
env, target, err := snapshotTargetIn(cwd, service, database, allDatabases, "restore")
if err != nil {
return err
}
target := snapshotTarget(env, allDatabases)
if !serviceops.SnapshotSupported(target.Service, target.AllDatabases) {
return fmt.Errorf("the %q service does not declare database snapshots", env.service)
}
if !target.AllDatabases && target.Database == "" {
return fmt.Errorf("no database resolved — pass --database, or --all-databases to restore the whole service")
}

scope := target.Database
if allDatabases {
Expand Down Expand Up @@ -225,6 +211,24 @@ func runDbSnapshotRm(name, service, database string, allDatabases bool) error {
return nil
}

// snapshotTargetIn resolves the database a snapshot or restore in cwd acts on,
// refusing a service without snapshots or a target it could not name. verb
// only words the error.
func snapshotTargetIn(cwd, service, database string, allDatabases bool, verb string) (*dbEnv, serviceops.SnapshotTarget, error) {
env, err := resolveDBForSnapshot(cwd, service, database, allDatabases)
if err != nil {
return nil, serviceops.SnapshotTarget{}, err
}
target := snapshotTarget(env, allDatabases)
if !serviceops.SnapshotSupported(target.Service, target.AllDatabases) {
return nil, target, fmt.Errorf("the %q service does not declare database snapshots", env.service)
}
if !target.AllDatabases && target.Database == "" {
return nil, target, fmt.Errorf("no database resolved — pass --database, or --all-databases to %s the whole service", verb)
}
return env, target, nil
}

// resolveDBForSnapshot resolves the project DB. All-databases operations only
// need the service, so they use lenient resolution that tolerates a missing
// database name.
Expand Down
64 changes: 8 additions & 56 deletions internal/cli/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package cli
import (
"fmt"

"github.com/geodro/lerd/internal/certs"
"github.com/geodro/lerd/internal/config"
"github.com/geodro/lerd/internal/feedback"
"github.com/geodro/lerd/internal/grouping"
"github.com/geodro/lerd/internal/linker"
"github.com/geodro/lerd/internal/nginx"
"github.com/geodro/lerd/internal/podman"
"github.com/geodro/lerd/internal/siteops"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -96,35 +93,12 @@ func runDomainAdd(_ *cobra.Command, args []string) error {
// Sync to .lerd.yaml.
_ = config.SyncProjectDomains(site.Path, site.Domains, cfg.DNS.TLD)

// Regenerate vhost (file stays named after primary domain).
if err := siteops.RegenerateSiteVhost(site, oldPrimary); err != nil {
warnings, err := grouping.ApplyDomainChange(site, oldPrimary)
if err != nil {
return err
}

// If secured, force-reissue the cert through the worktree-aware helper
// so the SAN list picks up the new domain without dropping any existing
// worktree subdomains (e.g. <branch>.<primary>). A bare IssueCertForce
// here would clobber those worktree SANs.
if site.Secured {
if err := certs.ReissueCertForWorktree(*site); err != nil {
feedback.Warn("reissuing certificate: %v", err)
}
}

if err := podman.WriteContainerHosts(); err != nil {
feedback.Warn("updating container hosts file: %v", err)
}

nginx.ReloadAndSettleOrWarn("")

if err := siteops.SyncEnvIfPrimaryChanged(site, oldPrimary); err != nil {
feedback.Warn("syncing .env to new primary domain: %v", err)
}

if site.IsGroupMain() {
if err := grouping.CascadeMainDomainChange(site); err != nil {
feedback.Warn("cascading group domain change: %v", err)
}
for _, w := range warnings {
feedback.Warn("%v", w)
}

feedback.Begin()
Expand Down Expand Up @@ -171,34 +145,12 @@ func runDomainRemove(_ *cobra.Command, args []string) error {
// Sync to .lerd.yaml, dropping the removed domain so it doesn't re-register.
_ = config.ReplaceProjectDomain(site.Path, site.Domains, fullDomain, cfg.DNS.TLD)

// If the primary domain changed (we removed the old primary), rename the vhost file.
if err := siteops.RegenerateSiteVhost(site, oldPrimary); err != nil {
warnings, err := grouping.ApplyDomainChange(site, oldPrimary)
if err != nil {
return err
}

// If secured, force-reissue the cert through the worktree-aware helper
// so the SAN list drops the removed domain without losing any existing
// worktree subdomains.
if site.Secured {
if err := certs.ReissueCertForWorktree(*site); err != nil {
feedback.Warn("reissuing certificate: %v", err)
}
}

if err := podman.WriteContainerHosts(); err != nil {
feedback.Warn("updating container hosts file: %v", err)
}

nginx.ReloadAndSettleOrWarn("")

if err := siteops.SyncEnvIfPrimaryChanged(site, oldPrimary); err != nil {
feedback.Warn("syncing .env to new primary domain: %v", err)
}

if site.IsGroupMain() {
if err := grouping.CascadeMainDomainChange(site); err != nil {
feedback.Warn("cascading group domain change: %v", err)
}
for _, w := range warnings {
feedback.Warn("%v", w)
}

feedback.Begin()
Expand Down
Loading
Loading