diff --git a/pkg/arvo/gen/hood/story-init.hoon b/pkg/arvo/gen/hood/story-init.hoon new file mode 100644 index 0000000000..d682864480 --- /dev/null +++ b/pkg/arvo/gen/hood/story-init.hoon @@ -0,0 +1,23 @@ +:: story: Create a story file for a given desk, optionally overwriting +:: +:::: + :: +/- *story +:- %say +|= $: [now=@da eny=@uvJ bec=beak] + [[~] =desk overwrite=_| ~] + == +=/ our p.bec +=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide +?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk) + ~& >> "Error: desk {} does not exist." + helm-pass+[%d %noop ~] +=/ existing-story .^(? %cu /(scot %p our)/[desk]/(scot %da now)/story) +?: ?&(existing-story !overwrite) + ~& >> "Error: /{(trip (slav %tas desk))}/story already exists." + ~& >> "To forcibly overwrite, use `=overwrite %.y`" + :: XX could use a better way to noop + helm-pass+[%d %noop ~] +=| tale=story +:- %helm-pass +[%c [%info desk %& [/story %ins story+!>(tale)]~]] \ No newline at end of file diff --git a/pkg/arvo/gen/hood/story-remove.hoon b/pkg/arvo/gen/hood/story-remove.hoon new file mode 100644 index 0000000000..8a3fe4c171 --- /dev/null +++ b/pkg/arvo/gen/hood/story-remove.hoon @@ -0,0 +1,36 @@ +:: story: Remove any commit message(s) for a given commit +:: +:: Optionally targeting a specific desk or prose +:: +:::: + :: +/- *story +:- %say +|= $: [now=@da eny=@uvJ bec=beak] + [[cas=cash ~] =desk prz=prose ~] + == +=/ our p.bec +=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide +=? cas =(*case cas) r.bec :: use case from beak if cas not provided +?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk) + ~& >> "Error: desk {} does not exist." + helm-pass+[%d %noop ~] +=/ tak=tako:clay + ?: ?=([%tako tako:clay] cas) + p.cas + ?: !.^(? %cs /(scot %p our)/[desk]/(scot cas)/case) + ~& >> "Error: invalid case {} provided" + !! + .^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~) +:: +=/ pax /(scot %p our)/[desk]/(scot %da now)/story +?: !.^(? %cu pax) + ~& >> "Error: No story file found. Please use |story-init to create one." + helm-pass+[%d %noop ~] +=/ tale=story .^(story %cx pax) +=. tale + ?: =(*prose prz) + (~(del by tale) tak) + (~(del ju tale) tak prz) +:- %helm-pass +[%c [%info desk %& [/story %ins story+!>(tale)]~]] \ No newline at end of file diff --git a/pkg/arvo/gen/hood/story-write.hoon b/pkg/arvo/gen/hood/story-write.hoon new file mode 100644 index 0000000000..5c213dad1f --- /dev/null +++ b/pkg/arvo/gen/hood/story-write.hoon @@ -0,0 +1,34 @@ +:: story: Attach a commit message (to the last commit by default) +:: +:: Optionally takes a case and desk +:: +:::: + :: +/- *story +:- %say +|= $: [now=@da eny=@uvJ bec=beak] + [[title=@t body=$@(~ [p=@t ~])] =desk cas=cash ~] + == +=/ our p.bec +=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide +=? cas =(*case cas) r.bec :: use case from beak if cas not provided +?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk) + ~& >> "Error: desk {} does not exist." + helm-pass+[%d %noop ~] +=/ tak=tako:clay + ?: ?=([%tako tako:clay] cas) + p.cas + ?: !.^(? %cs /(scot %p our)/[desk]/(scot cas)/case) + ~& >> "Error: invalid case {} provided" + !! + .^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~) +:: +=/ pax /(scot %p our)/[desk]/(scot %da now)/story +?: !.^(? %cu pax) + ~& >> "Error: No story file found. Please use |story-init to create one." + helm-pass+[%d %noop ~] +=/ tale=story .^(story %cx /(scot %p our)/[desk]/(scot %da now)/story) +=/ =prose [title ?~(body '' p.body)] +=. tale (~(put ju tale) tak prose) +:- %helm-pass +[%c [%info desk %& [/story %ins story+!>(tale)]~]] \ No newline at end of file diff --git a/pkg/arvo/gen/story-list.hoon b/pkg/arvo/gen/story-list.hoon new file mode 100644 index 0000000000..bc46a44387 --- /dev/null +++ b/pkg/arvo/gen/story-list.hoon @@ -0,0 +1,23 @@ +:: story: List unordered commit messages for the given desk, including orphans +:: +:::: + :: +/- *story +:- %say +|= $: [now=@da eny=@uvJ bec=beak] + [[~] =desk ~] + == +=/ our p.bec +=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide +=/ cas r.bec :: use case from beak +=/ pax /(scot %p our)/[desk]/(scot cas)/story +?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk) + tang+[leaf+"Error: desk {} does not exist." ~] +?: !.^(? %cu pax) + tang+['Error: No story file found. Please use |story-init to create one.' ~] +=/ story-to-txt + .^($-(story wain) %cf /(scot %p our)/[desk]/(scot cas)/story/txt) +:: +=/ tale .^(story %cx pax) +=/ tale-text (story-to-txt tale) +tang+tale-text \ No newline at end of file diff --git a/pkg/arvo/gen/story-read.hoon b/pkg/arvo/gen/story-read.hoon new file mode 100644 index 0000000000..0809993cd3 --- /dev/null +++ b/pkg/arvo/gen/story-read.hoon @@ -0,0 +1,154 @@ +:: story: log commits in order +:: +:::: + :: +/- *story +/+ lib=story +:- %say +|= $: [now=@da eny=@uvJ bec=beak] + [[~] =desk ~] + == +|^ +=/ our p.bec +=? desk =(*^desk desk) q.bec :: use current desk if user didn't provide +=/ cas r.bec :: use case from beak +=/ pax /(scot %p our)/[desk]/(scot cas)/story +?: !(~(has in .^((set ^desk) %cd /(scot %p our)/$/(scot %da now))) desk) + tang+[leaf+"Error: desk {} does not exist." ~] +?: !.^(? %cu pax) + tang+['Error: No story file found. Please use |story-init to create one.' ~] +=/ tak .^(tako:clay %cs /(scot %p our)/[desk]/(scot cas)/tako/~) +=/ yak .^(yaki:clay %cs /(scot %p our)/[desk]/(scot cas)/yaki/(scot %uv tak)) +=/ tale .^(story %cx pax) +:- %tang +(story-read [our desk cas] yak tale) +:::: +:: Remarks: +:: +:: There are two recursions in the logging process: +:: 1. the outer loop `commit-loop` threads down into each commit by ancestor +:: 2. the inner loop `ancestor-loop` threads left-to-right on reverse-ancestors +:: +:: +story-read outputs a tang with the least-recent commits at the head +:: of the list, even though we want most-recent commits to print first. +:: But because dojo prints tangs in reverse, we don't flop the results. +:::: +++ story-read + |= [[our=ship syd=^desk cas=case] this-commit=yaki:clay tale=story] + ^- tang + :: TODO factor out /(scot %p our)/[syd]/(scot cas) + %- head :: result from state + =| state=[result=tang mergebase=(unit tako:clay)] + |- + ^- _state + =* commit-loop $ + =/ reverse-ancestors (flop p.this-commit) + |- + =* ancestor-loop $ + ?- reverse-ancestors + ~ + :: stop here and return the current message + =/ msg=(list cord) (msg-from-commit this-commit tale) + [(weld msg result.state) mergebase=~] + :: + [tako:clay ~] + =/ parent i.reverse-ancestors + =/ parent-commit + .^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv parent)) + :: + =/ msg + (msg-from-commit this-commit tale) + :: + :: If there is a mergebase and we are visting it right now: + :: stop here and clear the mergebase. + :: skip adding the mergebase's msg itself, + :: because it will be added through the other branch. + :: Otherwise, record the current message if exists and recur. + ?: ?&(?=(^ mergebase.state) =(u.mergebase.state r.this-commit)) + [result=result.state mergebase=~] + commit-loop(this-commit parent-commit, result.state (weld msg result.state)) + :: + [tako:clay tako:clay ~] + :: + :: mainline: ultimate base chain + :: nowline: relative mainline + :: sideline: side-chain, featurebranch + :: + :: From the context of e, commit c is on its relative mainline, or nowline, + :: while commit d is on its sideline. + :: + :: %base a--b-------------X :: mainline + :: %new \--c------e--/ :: nowline + :: %new2 \--d--/ :: sideline + :: + :: + =/ sideline i.reverse-ancestors + =/ mainline i.t.reverse-ancestors + :: XX base-tako ignores beak + :: + =/ mergebases + .^ (list tako:clay) %cs + (scot %p our) syd (scot cas) + /base-tako/(scot %uv mainline)/(scot %uv sideline) + == + :: + :: Take the first valid mergebase (by convention) if exists, else none + :: + =/ next-mergebase + ?~(mergebases ~ (some i.mergebases)) + :: + =/ sideline-commit + .^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv sideline)) + :: + =/ mainline-commit + .^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv mainline)) + :: + =/ msg=(list cord) + (msg-from-commit this-commit tale) + :: + :: 1 - process current commit + :: 2 - recur and queue processing on all commits on the sideline + :: 3 - recur and queue processing on all commits on the mainline + :: + :: Because mainline messages are cons'd to result last, they are + :: (by definition) towards the less recent side of the flopped list + :: + =. state [result=(weld msg result.state) mergebase=next-mergebase] :: 1 + =. state commit-loop(this-commit sideline-commit) :: 2 + =. state commit-loop(this-commit mainline-commit) :: 3 + state + :: + [tako:clay tako:clay tako:clay *] + :: ~& "in 3+ ancestor commit" + =/ sideline i.reverse-ancestors + =/ nowline i.t.reverse-ancestors + =/ mergebases + .^ (list tako:clay) %cs + (scot %p our) syd (scot cas) + /base-tako/(scot %uv nowline)/(scot %uv sideline) + == + :: + :: Take the first valid mergebase (by convention) if exists, else none + :: + =/ next-mergebase ?~(mergebases ~ (some i.mergebases)) + =/ sideline-commit + .^(yaki:clay %cs /(scot %p our)/[syd]/(scot cas)/yaki/(scot %uv sideline)) + =. mergebase.state next-mergebase + =. state commit-loop(this-commit sideline-commit) :: downward + =. state ancestor-loop(reverse-ancestors t.reverse-ancestors) :: rightward + state + == +:: +++ msg-from-commit + |= [commit=yaki:clay tale=story] + ^- (list cord) + =/ proses (~(get by tale) r.commit) + ?~ proses ~ + %- flop :: fixes formatting reversal in dojo + %- to-wain:format + %- crip + ;: welp + (tako-to-text:lib r.commit) + (proses-to-text:lib u.proses) + == +-- \ No newline at end of file diff --git a/pkg/arvo/lib/story.hoon b/pkg/arvo/lib/story.hoon new file mode 120000 index 0000000000..226ddf06f1 --- /dev/null +++ b/pkg/arvo/lib/story.hoon @@ -0,0 +1 @@ +../../base-dev/lib/story.hoon \ No newline at end of file diff --git a/pkg/arvo/mar/story.hoon b/pkg/arvo/mar/story.hoon new file mode 120000 index 0000000000..915f279d88 --- /dev/null +++ b/pkg/arvo/mar/story.hoon @@ -0,0 +1 @@ +../../base-dev/mar/story.hoon \ No newline at end of file diff --git a/pkg/arvo/sur/story.hoon b/pkg/arvo/sur/story.hoon new file mode 120000 index 0000000000..4624552bd3 --- /dev/null +++ b/pkg/arvo/sur/story.hoon @@ -0,0 +1 @@ +../../base-dev/sur/story.hoon \ No newline at end of file diff --git a/pkg/arvo/sys/vane/clay.hoon b/pkg/arvo/sys/vane/clay.hoon index 871bbe376b..c0e0f2deba 100644 --- a/pkg/arvo/sys/vane/clay.hoon +++ b/pkg/arvo/sys/vane/clay.hoon @@ -1145,6 +1145,9 @@ !>([0 *@da]) !>([let.dom t:(~(got by hut.ran) (~(got by hit.dom) let.dom))]) =+ nao=(case-to-aeon case.mun) + ?: ?=([%s case %case ~] mun) + :: case existence check + [``[%& %flag !>(!=(~ nao))] fod.dom.red] ?~(nao [~ fod.dom.red] (read-at-aeon:ze for u.nao mun)) :: :: Queue a move. @@ -2283,7 +2286,8 @@ continuation-yaki merged-yaki merges t.merges hut.ran (~(put by hut.ran) r.merged-yaki merged-yaki) - lat.rag (~(uni by lat.rag) lat.u.merge-result) + lat.rag (~(uni by lat.u.merge-result) lat.rag) + lat.ran (~(uni by lat.u.merge-result) lat.ran) parents [(~(got by hit.ali-dom) let.ali-dom) parents] == == @@ -4022,9 +4026,15 @@ ++ read-s |= [yon=aeon pax=path] ^- (unit (unit cage)) - ?. ?=([?(%yaki %blob %hash %cage %open %late %base) * *] pax) + ?. ?=([?(%tako %yaki %blob %hash %cage %open %late %base %base-tako %case) * *] pax) `~ ?- i.pax + %tako + =/ tak=(unit tako) (~(get by hit.dom) yon) + ?~ tak + ~ + ``tako+[-:!>(*tako) u.tak] + :: %yaki =/ yak=(unit yaki) (~(get by hut.ran) (slav %uv i.t.pax)) ?~ yak @@ -4059,6 +4069,21 @@ ``open+!>(prelude:(ford:fusion static-ford-args)) :: %late !! :: handled in +aver + %case !! :: handled in +aver + %base-tako + :: XX this ignores the given beak + :: maybe move to +aver? + ?> ?=(^ t.t.pax) + :^ ~ ~ %uvs !> + ^- (list @uv) + =/ tako-a (slav %uv i.t.pax) + =/ tako-b (slav %uv i.t.t.pax) + =/ yaki-a (~(got by hut.ran) tako-a) + =/ yaki-b (~(got by hut.ran) tako-b) + %+ turn ~(tap in (find-merge-points yaki-a yaki-b)) + |= =yaki + r.yaki + :: %base ?> ?=(^ t.t.pax) :^ ~ ~ %uvs !> diff --git a/pkg/base-dev/lib/story.hoon b/pkg/base-dev/lib/story.hoon new file mode 100644 index 0000000000..8cb3c2a936 --- /dev/null +++ b/pkg/base-dev/lib/story.hoon @@ -0,0 +1,112 @@ +/- *story +!: +^? +|% +:: XX generalize move to hoon.hoon +++ dif-ju + |= [a=story b=story] + ^- story + :: if 0 is the empty set, + :: a \ 0 = a + :: 0 \ b = 0 :: anything in 0 but not in b is by definition 0 + :: + ?: =(~ a) ~ + :: uno := (a-b) + (merged items in both a and b) + (b-a) + :: ret := (a-b) + (merged items in both a and b) + :: ret = (~(int by a) uno) :: preserve only the entries whose keys are in a + =/ uno=story + %- (~(uno by a) b) + |= [k=tako:clay proses-a=proses proses-b=proses] + ^- proses + (~(dif in proses-a) proses-b) + :: + =/ ret=story (~(int by a) uno) + :: normalizing step, remove any keys with null sets, + :: which can occur if proses-a == proses-b above + %- ~(gas by *story) + (skip ~(tap by ret) |=([k=* v=proses] ?=(~ v))) +:: +++ uni-ju + |= [a=story b=story] + ^- story + :: 0 + b = b + ?: =(~ a) b + %- (~(uno by a) b) + |= [k=tako:clay proses-a=proses proses-b=proses] + ^- proses + (~(uni in proses-a) proses-b) +:: +:: Canonical textual representation +:: +++ tako-to-text + |= [=tako:clay] + ^- tape + "commit: {<`@uv`tako>}\0a" +:: +++ proses-to-text + |= [=proses] + ^- tape + =/ proses-list=(list prose) ~(tap in proses) + ?: ?=(~ proses-list) "" + ?: ?=([prose ~] proses-list) + (prose-to-text i.proses-list) + %- tail + %^ spin `(list prose)`t.proses-list + (prose-to-text i.proses-list) + |= [prz=prose state=tape] + ^- [prose tape] + :- prz + ;: welp + state + "|||" + "\0a" + (prose-to-text prz) + == +:: +++ prose-to-text + |= prz=prose + =/ [title=@t body=@t] prz + ^- tape + ;: welp + "{(trip title)}" + "\0a\0a" + "{(trip body)}" + "\0a" + == +:: +:: Parsers +:: +++ parse-commit-hash + ;~ sfix + ;~ pfix (jest 'commit: ') + (cook @uv ;~(pfix (jest '0v') viz:ag)) + == + :: + (just '\0a') + == +:: +++ parse-title + ;~ sfix + (cook crip (star prn)) + (jest '\0a\0a') + == +:: +++ parse-body + %+ cook of-wain:format + %- star + ;~ less + ;~(pose (jest '|||\0a') (jest '---\0a')) + (cook crip ;~(sfix (star prn) (just '\0a'))) + == +:: +++ parse-prose ;~(plug parse-title parse-body) +++ parse-rest-proses (star ;~(pfix (jest '|||\0a') parse-prose)) +++ parse-proses (cook silt ;~(plug parse-prose parse-rest-proses)) +++ parse-chapter ;~(plug parse-commit-hash parse-proses) +++ parse-story + (cook ~(gas by *story) (star ;~(sfix parse-chapter (jest '---\0a')))) +:: +:: N.B: If conflicting messages are written individually, +:: instead of under the same commit, we will overwrite previous entries +:: with later ones due to the nature of gas:by. +-- diff --git a/pkg/base-dev/mar/story-diff.hoon b/pkg/base-dev/mar/story-diff.hoon new file mode 100644 index 0000000000..17d417d366 --- /dev/null +++ b/pkg/base-dev/mar/story-diff.hoon @@ -0,0 +1,16 @@ +:: +:::: + :: +/- *story +|_ =story-diff +:: +++ grad %noun +++ grow + |% + ++ noun story-diff + -- +++ grab :: convert from + |% + ++ noun ^story-diff :: make from %noun + -- +-- diff --git a/pkg/base-dev/mar/story.hoon b/pkg/base-dev/mar/story.hoon new file mode 100644 index 0000000000..bf07ac742b --- /dev/null +++ b/pkg/base-dev/mar/story.hoon @@ -0,0 +1,70 @@ +/- *story +/+ *story +|_ tale=story +++ grad + |% + ++ form %story-diff + ++ diff + |= tory=story + ^- story-diff + :: Given new story (tory), old story (tale), compute the diff + :: additions = new - old + :: deletions = old - new + [(dif-ju tory tale) (dif-ju tale tory)] + ++ pact + |= dif=story-diff + :: Compute the new story after applying dif to tale. + :: + ^- story + =. tale (uni-ju tale additions.dif) + =. tale (dif-ju tale deletions.dif) + tale + ++ join + |= [ali=story-diff bob=story-diff] + ^- (unit story-diff) + =/ joined-additions (uni-ju additions.ali additions.bob) + =/ joined-deletions (uni-ju deletions.ali deletions.bob) + :: + :: In a true join, we'd do a set intersection on the keys. + :: If they're not equal, we have a conflict. + :: In this case, we'd produce null and kick the flow to +mash + :: + %- some + [joined-additions joined-deletions] + ++ mash + :: called by meld, force merge, annotating conflicts + |= $: [als=ship ald=desk ali=story-diff] + [bos=ship bod=desk bob=story-diff] + == + ^- story-diff + (need (join ali bob)) :: XX temporary, only because join doesn't fail + -- +:: +++ grow :: convert to + |% :: + ++ mime :: to %mime + [/text/x-urb-story (as-octs:mimes:html (of-wain:format txt))] + ++ txt + ^- wain + %- snoc :_ '' :: ensures terminating newline is present + %+ murn ~(tap by tale) + |= [[=tako:clay =proses]] + ^- (unit cord) + ?~ proses ~ + %- some + %- crip + ;: welp + (tako-to-text tako) + (proses-to-text proses) + "---" + == + -- +++ grab + |% :: convert from + ++ noun story :: clam from %noun + ++ mime :: retrieve from %mime + |= [p=mite q=octs] + =/ story-text `@t`q.q + `story`(rash story-text parse-story) + -- +-- diff --git a/pkg/base-dev/sur/story.hoon b/pkg/base-dev/sur/story.hoon new file mode 100644 index 0000000000..1336a616ba --- /dev/null +++ b/pkg/base-dev/sur/story.hoon @@ -0,0 +1,9 @@ +^? +|% ++$ prose [title=@t body=@t] ++$ proses (set prose) ++$ story (jug tako:clay prose) :: set len > 1 means conflicting messages have been assigned ++$ chapter [tako:clay proses] :: prose entry type ++$ cash $%([%tako p=tako:clay] case) :: used in generators to accept a tako directly as well ++$ story-diff [additions=story deletions=story] +-- \ No newline at end of file