исправлены находки ревью проектной стороны

- манифест читается так, как записан: решётка внутри строки не открывает
  комментарий, скобка внутри комментария не закрывает массив, имя внутри
  комментария не становится подпиской; новый ключ встаёт после массива,
  а не внутрь него
- всё записываемое проходит через manifest.Quote — обратный слэш в пути
  делал файл, который инструмент сам не читает
- маркер локальной части переехал в doc и пропускает огороженные блоки:
  процитированный в примере маркер больше не считается границей, а копия
  без маркера не перезаписывается молча
- лишний позиционный аргумент отсекается: flag прекращал разбор и прятал
  флаги после себя, из-за чего pull, list и check игнорировали --for
- заведены тесты проверок копий, включая молчание на исправной копии
This commit is contained in:
av
2026-07-27 21:16:29 +03:00
parent 23d88c4048
commit b6b0976c19
19 changed files with 904 additions and 197 deletions
+26 -28
View File
@@ -24,10 +24,10 @@ import (
"git.vakhrushev.me/av/convy/internal/suite"
)
// LocalMarker is the one piece of markup inside a copy that means something to
// the tool. It is single and nameless, so there is no name to be orphaned by a
// rename.
const LocalMarker = "<!-- conv:local -->"
// LocalMarker is the boundary between what the suite wrote and what the
// repository wrote. It is defined once, next to the parsing that has to respect
// it, and named again here because assembly is where it is placed.
const LocalMarker = doc.LocalMarker
// Copy is the outcome of assembling one topic for one component.
type Copy struct {
@@ -68,6 +68,7 @@ func Assemble(s *suite.Suite, root string, c manifest.Component, topic string) (
made.Layers = append(made.Layers, d.Path)
}
local := LocalMarker + "\n"
existing, err := os.ReadFile(name)
switch {
case os.IsNotExist(err):
@@ -75,12 +76,12 @@ func Assemble(s *suite.Suite, root string, c manifest.Component, topic string) (
case err != nil:
return Copy{}, err
default:
if err := ours(rel, string(existing), topic); err != nil {
kept, err := ours(rel, string(existing), topic)
if err != nil {
return Copy{}, err
}
local = kept
}
local := localPart(string(existing))
made.Kept = strings.TrimSpace(strings.TrimPrefix(local, LocalMarker)) != ""
body := Render(topic, layers, s.Vocab) + "\n\n" + local
@@ -123,37 +124,34 @@ func Reading(langRoot, path, root, dir string) (string, error) {
return rel, nil
}
// ours refuses to overwrite a file that is not a copy of this topic. A file
// whose origin key was taken away has stopped being a copy and become a
// document of the repository, and the tool has no business rewriting it.
func ours(rel, existing, topic string) error {
// ours decides whether a file standing in the way may be rewritten, and returns
// the part of it that survives: everything from the marker down.
//
// Three ways it may not. A file whose origin key was taken away has stopped
// being a copy and become a document of the repository. A file carrying the
// origin of another topic is another copy that would be buried by this one. And
// a file with no marker cannot be rewritten either — the marker is always
// placed by the assembler, so a copy without one was edited by hand, and
// everything in it counts as suite text that assembly would silently replace.
func ours(rel, existing, topic string) (string, error) {
d, err := doc.Parse(rel, existing)
if err != nil {
return fmt.Errorf("%s is in the way and cannot be read: %w", rel, err)
return "", fmt.Errorf("%s is in the way and cannot be read: %w", rel, err)
}
switch {
case d.Front.Origin == "" && d.Front.Topic == "" && d.Front.Prefix == "":
return fmt.Errorf("%s carries no origin key: it is a document of the repository rather than a copy, and assembly would overwrite it", rel)
return "", fmt.Errorf("%s carries no origin key: it is a document of the repository rather than a copy, and assembly would overwrite it", rel)
case d.Front.Origin == "":
return fmt.Errorf("%s carries no origin key, while it does carry the front matter of a suite file: it looks like a layer put here by hand", rel)
return "", fmt.Errorf("%s carries no origin key, while it does carry the front matter of a suite file: it looks like a layer put here by hand", rel)
case d.Front.Origin != topic:
return fmt.Errorf("%s is a copy of the topic %q, and the topic %q would be assembled into the same file", rel, d.Front.Origin, topic)
return "", fmt.Errorf("%s is a copy of the topic %q, and the topic %q would be assembled into the same file", rel, d.Front.Origin, topic)
}
return nil
}
// localPart returns everything from the marker down, together with the marker.
// A file that has none gets one: the marker is placed by the assembler, and a
// copy without it has nowhere to put a derogation.
func localPart(existing string) string {
lines := strings.Split(existing, "\n")
for i, line := range lines {
if strings.TrimSpace(line) != LocalMarker {
continue
}
return strings.TrimRight(strings.Join(lines[i:], "\n"), "\n") + "\n"
marker := d.Marker()
if marker == 0 {
return "", fmt.Errorf("%s carries no %s marker, and the assembler always leaves one: whatever is in the file was written above the boundary and would be replaced without trace", rel, LocalMarker)
}
return LocalMarker + "\n"
return strings.TrimRight(d.Below(marker), "\n") + "\n", nil
}
// Render lays out the part of a copy that comes from the suite: the front