Loading i18n.go +2 −0 Original line number Diff line number Diff line Loading @@ -111,6 +111,7 @@ commandes new, projects et cache-clear qui peuvent être lancées aussi en dehor "err.bundle_install_failed": "échec de l'installation du bundle %s: %w", "err.no_latest_release": "pas de dernière release disponible", "err.unknown_version": "version inconnue", "err.unsupported_max_version": "version de MaX non prise en charge: %s (version minimale: %s)", "err.server_start_timeout": "délai dépassé au démarrage du serveur HTTP sur %s:%d", "err.invalid_path": "%s n'est pas un chemin valide", "err.invalid_bundle_archive": "%s n'est pas un bundle", Loading Loading @@ -222,6 +223,7 @@ which can also be run outside a MaX directory.`, "err.bundle_install_failed": "bundle installation failed for %s: %w", "err.no_latest_release": "no latest release available", "err.unknown_version": "unknown version", "err.unsupported_max_version": "unsupported MaX version: %s (minimum: %s)", "err.server_start_timeout": "timeout while starting HTTP server on %s:%d", "err.invalid_path": "%s is not a valid path", "err.invalid_bundle_archive": "%s is not a bundle", Loading main.go +112 −9 Original line number Diff line number Diff line Loading @@ -46,7 +46,8 @@ var fallbackReleases []byte const version = "0.3.3" const maxConfigFile = "config.xml" const baseXDistro = "https://files.basex.org/releases/11.7/BaseX117.zip" const minSupportedMaxVersion = "0.0.5" const baseXDistro = "https://files.basex.org/releases/12.2/BaseX122.zip" const saxonDistro = "https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/10.8/Saxon-HE-10.8.jar" const webPort = 8080 const webHost = "127.0.0.1" Loading @@ -57,7 +58,7 @@ const gitlabAPIReadToken = "glpat-5BCYCid1WaxxBgax-7VW" const configInitTemplate = `<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns="http://certic.unicaen.fr/max/ns/1.0" env="dev" vocabulary-bundle="max-dumb-xml"> <version name="0.0.2" url="https://git.unicaen.fr/pdn-certic/max-v2/-/archive/0.0.2/max-v2-0.0.2.zip"/> <version name="0.0.5" url="https://git.unicaen.fr/pdn-certic/max-v2/-/archive/0.0.5/max-v2-0.0.5.zip"/> <languages> <language>fr</language> <language>en</language> Loading Loading @@ -758,6 +759,7 @@ func maxReleases() ([]release, error) { if resp.StatusCode >= 200 && resp.StatusCode < 300 { body, _ := io.ReadAll(resp.Body) if errDecode := json.Unmarshal(body, &releasesData); errDecode == nil { releasesData = filterSupportedMaxReleases(releasesData) return } } Loading @@ -766,7 +768,9 @@ func maxReleases() ([]release, error) { if err := json.Unmarshal(fallbackReleases, &releasesData); err != nil { releasesErr = err return } releasesData = filterSupportedMaxReleases(releasesData) }) return releasesData, releasesErr } Loading Loading @@ -801,6 +805,80 @@ func latestMaxRelease() (*release, error) { return maxRelease(releases[0].Name) } func filterSupportedMaxReleases(releases []release) []release { filtered := make([]release, 0, len(releases)) for _, r := range releases { if isSupportedMaxVersion(r.Name) { filtered = append(filtered, r) } } return filtered } func isSupportedMaxVersion(raw string) bool { return compareVersion(raw, minSupportedMaxVersion) >= 0 } func compareVersion(left, right string) int { lv, okLeft := parseVersion(left) rv, okRight := parseVersion(right) if !okLeft || !okRight { return -1 } for i := 0; i < len(lv) || i < len(rv); i++ { leftPart := 0 if i < len(lv) { leftPart = lv[i] } rightPart := 0 if i < len(rv) { rightPart = rv[i] } switch { case leftPart < rightPart: return -1 case leftPart > rightPart: return 1 } } return 0 } func parseVersion(raw string) ([]int, bool) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil, false } if strings.HasPrefix(trimmed, "v") || strings.HasPrefix(trimmed, "V") { trimmed = trimmed[1:] } main := strings.SplitN(trimmed, "-", 2)[0] main = strings.SplitN(main, "+", 2)[0] parts := strings.Split(main, ".") if len(parts) < 3 { return nil, false } out := make([]int, 0, len(parts)) for _, part := range parts { if part == "" { return nil, false } value, err := strconv.Atoi(part) if err != nil { return nil, false } out = append(out, value) } return out, true } func displayVersion(raw string) string { if strings.TrimSpace(raw) == "" { return T("msg.unknown_title") } return raw } func projectsPath() string { return filepath.Join(userMaxDir, "projects.json") } Loading Loading @@ -956,7 +1034,6 @@ func dirIsMax(directory string) (bool, error) { if start.Name.Local == "configuration" { for _, a := range start.Attr { if a.Name.Local == "xmlns" && a.Value == "http://certic.unicaen.fr/max/ns/1.0" { _ = pingProjects(abs, nil) return true, nil } } Loading @@ -976,8 +1053,31 @@ func checkDirIsMax(directory string) error { if !ok { return errors.New(T("err.not_max_directory")) } if err := checkProjectMaxVersionSupported(directory); err != nil { return err } abs, err := filepath.Abs(directory) if err != nil { return err } _ = pingProjects(abs, nil) return nil } func checkProjectMaxVersionSupported(directory string) error { abs, err := filepath.Abs(directory) if err != nil { return err } config, err := loadProjectConfig(filepath.Join(abs, maxConfigFile)) if err != nil { return err } if isSupportedMaxVersion(config.Cfg.Version.Name) { return nil } return fmt.Errorf(T("err.unsupported_max_version"), displayVersion(config.Cfg.Version.Name), minSupportedMaxVersion) } func ensureAvailableMaxDirectory(directory string) (string, bool, error) { if strings.TrimSpace(directory) == "" { Loading Loading @@ -1446,6 +1546,9 @@ func syncMaxInstance(rootDirectory string, verbose bool) error { if err != nil { return err } if !isSupportedMaxVersion(config.Cfg.Version.Name) { return fmt.Errorf(T("err.unsupported_max_version"), displayVersion(config.Cfg.Version.Name), minSupportedMaxVersion) } dotMaxDir := filepath.Join(rootDirectory, ".max") if !dirExists(dotMaxDir) || config.maxVersionFromMaxDir() != config.Cfg.Version.Name { Loading Loading @@ -1485,17 +1588,20 @@ func syncMaxInstance(rootDirectory string, verbose bool) error { } func runNew(directory string, interactive bool) error { if _, err := ensureJava(); err != nil { return err } rootDirectory, isMax, err := ensureAvailableMaxDirectory(directory) if err != nil { return err } if isMax { if err := checkProjectMaxVersionSupported(rootDirectory); err != nil { return err } fmt.Println(T("msg.directory_already_max")) return nil } if _, err := ensureJava(); err != nil { return err } var values *initValues if interactive { Loading Loading @@ -2354,9 +2460,6 @@ func formatCommand(cmd *exec.Cmd) string { } func runSync(directory string) error { if _, err := ensureJava(); err != nil { return err } rootDirectory, isMax, err := ensureAvailableMaxDirectory(directory) if err != nil { return err Loading main_test.go +51 −0 Original line number Diff line number Diff line Loading @@ -110,6 +110,57 @@ func TestProjectsJSONStorageLifecycle(t *testing.T) { } } func TestCheckDirIsMaxRejectsUnsupportedMaxVersion(t *testing.T) { previousLocale := currentLocale setLocale(localeEN) t.Cleanup(func() { setLocale(previousLocale) }) tmpHome := t.TempDir() prevHome := userMaxDir userMaxDir = tmpHome t.Cleanup(func() { userMaxDir = prevHome }) projectDir := filepath.Join(t.TempDir(), "project") if err := os.MkdirAll(projectDir, 0o755); err != nil { t.Fatal(err) } config := strings.ReplaceAll(configInitTemplate, "0.0.5", "0.0.4") if err := os.WriteFile(filepath.Join(projectDir, maxConfigFile), []byte(config), 0o644); err != nil { t.Fatal(err) } err := checkDirIsMax(projectDir) if err == nil { t.Fatal("expected unsupported version error") } if !strings.Contains(err.Error(), "0.0.4") || !strings.Contains(err.Error(), minSupportedMaxVersion) { t.Fatalf("expected error to mention configured and minimum versions, got: %v", err) } } func TestFilterSupportedMaxReleasesDropsVersionsBelowMinimum(t *testing.T) { releases := []release{ {Name: "0.0.6"}, {Name: "0.0.4"}, {Name: "v0.0.5"}, {Name: "invalid"}, } filtered := filterSupportedMaxReleases(releases) got := make([]string, 0, len(filtered)) for _, r := range filtered { got = append(got, r.Name) } want := []string{"0.0.6", "v0.0.5"} if strings.Join(got, ",") != strings.Join(want, ",") { t.Fatalf("unexpected filtered releases: want %v, got %v", want, got) } } func TestExtractTarGzPreservesExecutableBit(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("executable permission bits are not relevant on windows") Loading max_releases.json +1 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
i18n.go +2 −0 Original line number Diff line number Diff line Loading @@ -111,6 +111,7 @@ commandes new, projects et cache-clear qui peuvent être lancées aussi en dehor "err.bundle_install_failed": "échec de l'installation du bundle %s: %w", "err.no_latest_release": "pas de dernière release disponible", "err.unknown_version": "version inconnue", "err.unsupported_max_version": "version de MaX non prise en charge: %s (version minimale: %s)", "err.server_start_timeout": "délai dépassé au démarrage du serveur HTTP sur %s:%d", "err.invalid_path": "%s n'est pas un chemin valide", "err.invalid_bundle_archive": "%s n'est pas un bundle", Loading Loading @@ -222,6 +223,7 @@ which can also be run outside a MaX directory.`, "err.bundle_install_failed": "bundle installation failed for %s: %w", "err.no_latest_release": "no latest release available", "err.unknown_version": "unknown version", "err.unsupported_max_version": "unsupported MaX version: %s (minimum: %s)", "err.server_start_timeout": "timeout while starting HTTP server on %s:%d", "err.invalid_path": "%s is not a valid path", "err.invalid_bundle_archive": "%s is not a bundle", Loading
main.go +112 −9 Original line number Diff line number Diff line Loading @@ -46,7 +46,8 @@ var fallbackReleases []byte const version = "0.3.3" const maxConfigFile = "config.xml" const baseXDistro = "https://files.basex.org/releases/11.7/BaseX117.zip" const minSupportedMaxVersion = "0.0.5" const baseXDistro = "https://files.basex.org/releases/12.2/BaseX122.zip" const saxonDistro = "https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/10.8/Saxon-HE-10.8.jar" const webPort = 8080 const webHost = "127.0.0.1" Loading @@ -57,7 +58,7 @@ const gitlabAPIReadToken = "glpat-5BCYCid1WaxxBgax-7VW" const configInitTemplate = `<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns="http://certic.unicaen.fr/max/ns/1.0" env="dev" vocabulary-bundle="max-dumb-xml"> <version name="0.0.2" url="https://git.unicaen.fr/pdn-certic/max-v2/-/archive/0.0.2/max-v2-0.0.2.zip"/> <version name="0.0.5" url="https://git.unicaen.fr/pdn-certic/max-v2/-/archive/0.0.5/max-v2-0.0.5.zip"/> <languages> <language>fr</language> <language>en</language> Loading Loading @@ -758,6 +759,7 @@ func maxReleases() ([]release, error) { if resp.StatusCode >= 200 && resp.StatusCode < 300 { body, _ := io.ReadAll(resp.Body) if errDecode := json.Unmarshal(body, &releasesData); errDecode == nil { releasesData = filterSupportedMaxReleases(releasesData) return } } Loading @@ -766,7 +768,9 @@ func maxReleases() ([]release, error) { if err := json.Unmarshal(fallbackReleases, &releasesData); err != nil { releasesErr = err return } releasesData = filterSupportedMaxReleases(releasesData) }) return releasesData, releasesErr } Loading Loading @@ -801,6 +805,80 @@ func latestMaxRelease() (*release, error) { return maxRelease(releases[0].Name) } func filterSupportedMaxReleases(releases []release) []release { filtered := make([]release, 0, len(releases)) for _, r := range releases { if isSupportedMaxVersion(r.Name) { filtered = append(filtered, r) } } return filtered } func isSupportedMaxVersion(raw string) bool { return compareVersion(raw, minSupportedMaxVersion) >= 0 } func compareVersion(left, right string) int { lv, okLeft := parseVersion(left) rv, okRight := parseVersion(right) if !okLeft || !okRight { return -1 } for i := 0; i < len(lv) || i < len(rv); i++ { leftPart := 0 if i < len(lv) { leftPart = lv[i] } rightPart := 0 if i < len(rv) { rightPart = rv[i] } switch { case leftPart < rightPart: return -1 case leftPart > rightPart: return 1 } } return 0 } func parseVersion(raw string) ([]int, bool) { trimmed := strings.TrimSpace(raw) if trimmed == "" { return nil, false } if strings.HasPrefix(trimmed, "v") || strings.HasPrefix(trimmed, "V") { trimmed = trimmed[1:] } main := strings.SplitN(trimmed, "-", 2)[0] main = strings.SplitN(main, "+", 2)[0] parts := strings.Split(main, ".") if len(parts) < 3 { return nil, false } out := make([]int, 0, len(parts)) for _, part := range parts { if part == "" { return nil, false } value, err := strconv.Atoi(part) if err != nil { return nil, false } out = append(out, value) } return out, true } func displayVersion(raw string) string { if strings.TrimSpace(raw) == "" { return T("msg.unknown_title") } return raw } func projectsPath() string { return filepath.Join(userMaxDir, "projects.json") } Loading Loading @@ -956,7 +1034,6 @@ func dirIsMax(directory string) (bool, error) { if start.Name.Local == "configuration" { for _, a := range start.Attr { if a.Name.Local == "xmlns" && a.Value == "http://certic.unicaen.fr/max/ns/1.0" { _ = pingProjects(abs, nil) return true, nil } } Loading @@ -976,8 +1053,31 @@ func checkDirIsMax(directory string) error { if !ok { return errors.New(T("err.not_max_directory")) } if err := checkProjectMaxVersionSupported(directory); err != nil { return err } abs, err := filepath.Abs(directory) if err != nil { return err } _ = pingProjects(abs, nil) return nil } func checkProjectMaxVersionSupported(directory string) error { abs, err := filepath.Abs(directory) if err != nil { return err } config, err := loadProjectConfig(filepath.Join(abs, maxConfigFile)) if err != nil { return err } if isSupportedMaxVersion(config.Cfg.Version.Name) { return nil } return fmt.Errorf(T("err.unsupported_max_version"), displayVersion(config.Cfg.Version.Name), minSupportedMaxVersion) } func ensureAvailableMaxDirectory(directory string) (string, bool, error) { if strings.TrimSpace(directory) == "" { Loading Loading @@ -1446,6 +1546,9 @@ func syncMaxInstance(rootDirectory string, verbose bool) error { if err != nil { return err } if !isSupportedMaxVersion(config.Cfg.Version.Name) { return fmt.Errorf(T("err.unsupported_max_version"), displayVersion(config.Cfg.Version.Name), minSupportedMaxVersion) } dotMaxDir := filepath.Join(rootDirectory, ".max") if !dirExists(dotMaxDir) || config.maxVersionFromMaxDir() != config.Cfg.Version.Name { Loading Loading @@ -1485,17 +1588,20 @@ func syncMaxInstance(rootDirectory string, verbose bool) error { } func runNew(directory string, interactive bool) error { if _, err := ensureJava(); err != nil { return err } rootDirectory, isMax, err := ensureAvailableMaxDirectory(directory) if err != nil { return err } if isMax { if err := checkProjectMaxVersionSupported(rootDirectory); err != nil { return err } fmt.Println(T("msg.directory_already_max")) return nil } if _, err := ensureJava(); err != nil { return err } var values *initValues if interactive { Loading Loading @@ -2354,9 +2460,6 @@ func formatCommand(cmd *exec.Cmd) string { } func runSync(directory string) error { if _, err := ensureJava(); err != nil { return err } rootDirectory, isMax, err := ensureAvailableMaxDirectory(directory) if err != nil { return err Loading
main_test.go +51 −0 Original line number Diff line number Diff line Loading @@ -110,6 +110,57 @@ func TestProjectsJSONStorageLifecycle(t *testing.T) { } } func TestCheckDirIsMaxRejectsUnsupportedMaxVersion(t *testing.T) { previousLocale := currentLocale setLocale(localeEN) t.Cleanup(func() { setLocale(previousLocale) }) tmpHome := t.TempDir() prevHome := userMaxDir userMaxDir = tmpHome t.Cleanup(func() { userMaxDir = prevHome }) projectDir := filepath.Join(t.TempDir(), "project") if err := os.MkdirAll(projectDir, 0o755); err != nil { t.Fatal(err) } config := strings.ReplaceAll(configInitTemplate, "0.0.5", "0.0.4") if err := os.WriteFile(filepath.Join(projectDir, maxConfigFile), []byte(config), 0o644); err != nil { t.Fatal(err) } err := checkDirIsMax(projectDir) if err == nil { t.Fatal("expected unsupported version error") } if !strings.Contains(err.Error(), "0.0.4") || !strings.Contains(err.Error(), minSupportedMaxVersion) { t.Fatalf("expected error to mention configured and minimum versions, got: %v", err) } } func TestFilterSupportedMaxReleasesDropsVersionsBelowMinimum(t *testing.T) { releases := []release{ {Name: "0.0.6"}, {Name: "0.0.4"}, {Name: "v0.0.5"}, {Name: "invalid"}, } filtered := filterSupportedMaxReleases(releases) got := make([]string, 0, len(filtered)) for _, r := range filtered { got = append(got, r.Name) } want := []string{"0.0.6", "v0.0.5"} if strings.Join(got, ",") != strings.Join(want, ",") { t.Fatalf("unexpected filtered releases: want %v, got %v", want, got) } } func TestExtractTarGzPreservesExecutableBit(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("executable permission bits are not relevant on windows") Loading