\ net2o distributed version control system

\ Copyright (C) 2016-2019   Bernd Paysan

\ This program is free software: you can redistribute it and/or modify
\ it under the terms of the GNU Affero General Public License as published by
\ the Free Software Foundation, either version 3 of the License, or
\ (at your option) any later version.

\ This program is distributed in the hope that it will be useful,
\ but WITHOUT ANY WARRANTY; without even the implied warranty of
\ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
\ GNU Affero General Public License for more details.

\ You should have received a copy of the GNU Affero General Public License
\ along with this program.  If not, see <http://www.gnu.org/licenses/>.

Variable dvcs-objects \ hash of objects

Variable dvcs-table

vocabulary project

cmd-class class
    scope: dvcs
    field: in-files$
    field: patch$
    field: out-files$

    method read
    method rm
    method rmdir
    method patch
    method write
    method unzip
    method ref
    
    }scope
end-class dvcs-abstract

dvcs-abstract class
    scope{ dvcs
    field: refs[]
    }scope
end-class dvcs-refs

dvcs-abstract class
    scope{ dvcs
    field: commits    \ msg class for commits
    field: searchs    \ msg class for searchs
    field: id$        \ commit id
    field: branch$
    field: message$   \ commit message
    field: fileref[]  \ file refs
    field: files#     \ snapshot config
    field: oldfiles#  \ old state to compare to
    field: out-fileoff
    field: fileentry$
    field: oldid$
    field: hash$
    field: type
    field: rmdirs[]   \ sorted array of dirs to be delete
    field: outfiles[] \ sorted array of files to write out

    }scope
    
    scope{ project \ per-project configuration values

    field: chain$
    field: revision$
    field: branch$
    field: project$

    }scope

end-class dvcs-class

scope{ dvcs

begin-structure filehash
    64field: timestamp
    wfield: perm
    0 +field name
end-structure

}scope

msg-class class
    field: id>patch#  \ convert an ID to a patch+reference list
    field: id>snap#   \ convert an ID to a snapshot (starting point)
    field: id$
    field: re$
    field: object$
end-class commit-class

msg-class class
    scope: match
    field: tag$
    field: flag
    field: id$
    }scope
end-class search-class

msg-class class
    scope: dvcs-log
    field: sig$
    field: tag$
    field: id$
    field: action$
    field: text$
    field: chain$
    field: urls[]
    }scope
end-class dvcs-log-class

: /name ( addr u -- addr' u' )
    [ hash#128 dvcs:name ]L /string ;
: fn-split ( hash+ts+perm+fn u -- hash+ts+perm u1 fname u2 )
    [ hash#128 dvcs:name ]L >r 2dup r@ umin 2swap r> /string ;

: .mode ( u -- )
    dup S_IFMT and 12 rshift "0pc3d5b7frlBsDEF" drop + c@ emit space
    S_IFMT invert and ['] . 8 base-execute ;

: .file+hash ( addr u -- )
    over hash#128 85type space  hash#128 /string
    over dvcs:timestamp le-64@ .ticks space
    over dvcs:perm le-uw@ .mode
    0 dvcs:name /string type cr ;

: +fileentry ( addr u o:dvcs -- )
    \G add a file entry and replace same file if it already exists
    dvcs( ." +f: " 2dup .file+hash ) fn-split dvcs:files# #! ;
: -fileentry ( addr u o:dvcs -- )
    dvcs( ." -f: " 2dup .file+hash ) /name dvcs:files# #free ;

: create-symlink-f ( addrdest udest addrlink ulink perm -- ) { perm }
    \G create symlink and overwrite existing file
    2over 2over symlink dup -1 = IF
	errno EEXIST = IF  drop
	    2dup delete-file throw 2over 2over symlink
	THEN
    THEN  ?ior 2drop 2drop ;

: create-file-f ( addr u addrfile ufile perm -- ) { perm }
    r/w create-file throw >r
    dvcs( ." write " dup . ." bytes" cr )
    r@ write-file throw
    r@ fileno perm fchmod ?ior
    r> close-file throw ;

: create-dir-f ( addr 0 addrdir udir perm -- ) { perm }
    2dup delete-file drop \ try deleting it as file
    2dup perm mkdir-parents
    dup file-exist# = IF  drop  ELSE  throw  THEN
    perm chmod ?ior
    2drop ;

S_IFMT $1000 invert and Constant S_IFMT?

: dvcs-outfile-name ( hash+perm-addr u1 fname u2 -- )
    2>r 2dup key| dvcs-objects #@ 2swap hash#128 /string
    drop dvcs:perm le-uw@ { perm } 2r>
    perm S_IFMT? and  case
	S_IFLNK              of  perm create-symlink-f  endof
	S_IFREG              of  perm create-file-f     endof
	S_IFDIR              of  perm create-dir-f      endof  \ no content in directory
	dvcs( ." unhandled type " hex. type space hex. drop cr 0 )else(
	2drop 2drop ) \ unhandled types
    endcase ;

\ encrypted hash stuff, using signature secret as PSK

\ probably needs padding...

: write-enc-hashed ( addr1 u1 -- addrhash85 u2 )
    $make { w^ enc-pad$ } enc-pad$ $@
    keyed-hash-out hash#128 ['] 85type $tmp 2>r
    enchash  2>r
    2dup c:encrypt
    ?.net2o/objects  2r> hash>filename  spit-file
    enc-pad$ $free 2r> ;

: enchash>filename ( hash1 u1 -- filename u2 )
    keyed-hash-out hash#128 smove
    enchash hash>filename ;

Variable patch-in$

: read-enc-hashed ( hash1 u1 -- )
    \ 2dup 85type space
    2dup enchash>filename patch-in$ $slurp-file
    patch-in$ $@ c:decrypt
    patch-in$ $@ >file-hash \ 2dup 85type cr
    str= 0= !!wrong-hash!! ;

: ?read-enc-hashed ( hash1 u1 -- addr u )
    2dup dvcs-objects #@ 2dup d0= IF
	2drop 2dup read-enc-hashed
	patch-in$ $@ 2swap dvcs-objects #!
    ELSE
	2drop 2drop
    THEN  last# cell+ $@ ;

\ in-memory file hash+contents database

: dvcs-outfile-hash ( baddr u1 fhash u2 -- )
    hash#128 umin dvcs-objects #! ;

: ?fileentry-hash ( -- )
    dvcs:fileentry$ $@ hash#128 umin
    2dup dvcs-objects #@ d0<> IF  2drop  EXIT  THEN
    read-enc-hashed
    patch-in$ $@ dvcs:fileentry$ $@ dvcs-outfile-hash ;

: dvcs-in-hash ( addr u -- )
    dvcs( ." +read: " 2dup 85type cr )
    dvcs-objects #@ over 0= !!dvcs-hash!!
    dvcs:in-files$ $+! ;

: filelist-print ( filelist -- )
    [: >r r@ cell+ $@ 85type space r> $@ type cr ;] #map ;
: filelist-out ( o:dvcs -- )
    ".n2o/files" [: >r dvcs:files# ['] filelist-print r> outfile-execute ;]
    new-file ;

: filelist-loop ( -- )
    BEGIN  refill  WHILE
	    source bl $split 2>r base85>$ 2r> dvcs:files# #!
    REPEAT ;
: filelist-in ( addr u o:dvcs -- )
    r/o open-file throw ['] filelist-loop execute-parsing-file ;

scope{ net2o-base

\g 
\g ### DVCS patch commands ###
\g 
\g DVCS metadata is stored in messages, containing message text, refs
\g and patchset objects. Patchset objects are constructed in a way
\g that makes identical transactions have the same hash.
\g 

reply-table $@ inherit-table dvcs-table

net2o' emit net2o: dvcs-read ( $:hash -- ) \g read in an object
    1 !!>=order? $> dvcs:read ;
+net2o: dvcs-rm ( $:hash+name -- ) \g delete file
    2 !!>=order? $> dvcs:rm ;
+net2o: dvcs-rmdir ( $:name -- ) \g delete directory
    4 !!>=order? $> dvcs:rmdir ;
+net2o: dvcs-patch ( $:diff len -- ) \g apply patch, len is the size of the result
    8 !!>order? $> dvcs:patch ;
+net2o: dvcs-write ( $:perm+name size -- ) \g write out file
    $10 !!>=order? $> dvcs:write ;
+net2o: dvcs-unzip ( $:diffgz size algo -- $:diff ) \g unzip an object
    1 !!>=order? 64>n $> dvcs:unzip ;
+net2o: dvcs-ref ( $:hash+perm+name -- ) \g external hash reference
    $10 !!>=order? $> dvcs:ref ;

}scope

dvcs-table $save

' dvcs-in-hash ( addr u -- ) dvcs-class is dvcs:read
:noname ( addr u -- )
    2dup hash#128 /string ?sane-file
    dvcs( ." -f: " 2dup forth:type forth:cr ) dvcs:files# #free
    hash#128 umin dvcs-in-hash ; dvcs-class is dvcs:rm
:noname ( addr u -- )
    2dup 2 /string ?sane-file 2drop
    dvcs( ." -f: " 2dup forth:type forth:cr ) dvcs:files# #free
; dvcs-class is dvcs:rmdir
:noname ( 64len addr u -- )
    dvcs:patch$ $! dvcs( ." -patch: " 64dup u64. )
    dvcs:out-fileoff off
    64dup config:patchlimit& 2@ d>64 64u> !!patch-limit!!
    dvcs:patch$ bpatch$len 64<> !!patch-size!! \ sanity check!
    dvcsfiles( ." ===== in files =====" cr dvcs:in-files$ $. cr )
    dvcs( ." ===== diff =====" cr
    dvcs:in-files$ dvcs:patch$ color-bpatch$2 )
    dvcs:out-files$ $free
    dvcs:in-files$ dvcs:patch$ ['] bpatch$2 dvcs:out-files$ $exec
    dvcsfiles( ." ===== " dvcs:out-files$ $@len u. ."  =====" cr
    dvcs:out-files$ $. ." ========================" cr )
; dvcs-class is dvcs:patch
:noname ( 64size addr u -- )
    2dup 2 /string ?sane-file 2drop
    2>r dvcs( ." -write: " 64dup u64. cr )
    64>n { fsize }
    dvcs:out-files$ $@ dvcs:out-fileoff @ safe/string fsize umin
    2dup >file-hash 2r> 2swap  dvcs:fileentry$ $free
    [: forth:type ticks { 64^ ts } ts 1 64s forth:type forth:type ;]
    dvcs:fileentry$ $exec dvcs:fileentry$ $@
    2dup +fileentry  dvcs-outfile-hash
    fsize dvcs:out-fileoff +! ; dvcs-class is dvcs:write
' !!FIXME!! ( 64size algo addr u --- ) dvcs-class is dvcs:unzip
:noname ( addr u -- ) \ hash+perm+name
    0 patch-in$ !@ >r
    dvcs:fileentry$ $free
    [: over hash#128 forth:type ticks { 64^ ts } ts 1 64s forth:type
	hash#128 /string forth:type ;] dvcs:fileentry$ $exec
    dvcs:fileentry$ $@ +fileentry
    ?fileentry-hash
    patch-in$ $free  r> patch-in$ !
; dvcs-class is dvcs:ref

\ DVCS refs are scanned for in patchsets, and then fetched

' 2drop dvcs-refs is dvcs:read
' 2drop dvcs-refs is dvcs:rm
' 2drop dvcs-refs is dvcs:rmdir
:noname 2drop 64drop ; dup dvcs-refs is dvcs:patch
dvcs-refs is dvcs:write
:noname 2drop drop 64drop ; dvcs-refs is dvcs:unzip
:noname ( addr u -- )
    hash#128 umin 2dup dvcs-objects #@ d0<> IF  2drop  EXIT  THEN
    2dup enchash>filename file-status nip no-file# <> IF  2drop  EXIT  THEN
    dvcs:refs[] $+[]! ; dvcs-refs is dvcs:ref

scope{ dvcs
: new-dvcs ( -- o )
    dvcs-class new >o  dvcs-table @ token-table !
    commit-class new >o  msg-table @ token-table !  o o>  dvcs:commits !
    search-class new >o  msg-table @ token-table !  o o>  dvcs:searchs !
    o o> ;
: new-dvcs-refs ( -- o )
    dvcs-refs new >o  dvcs-table @ token-table !  o o> ;
: clean-delta ( o:dvcs -- )
    in-files$ $free out-files$ $free patch$ $free ;
: dispose-commit ( o:commit -- )
    id$ $free  re$ $free  object$ $free  dispose ;
: dispose-search ( o:commit -- )
    match:id$ $free  match:tag$ $free  dispose ;
: dispose-dvcs-refs ( o:dvcs -- )
    dvcs:refs[] $[]free dispose ;
: dispose-dvcs ( o:dvcs -- )
    dvcs:branch$ $free  dvcs:message$ $free  dvcs:fileref[] $[]free
    dvcs:files# #frees  dvcs:oldfiles# #frees
    dvcs:rmdirs[] $[]off  dvcs:outfiles[] $[]off
    clean-delta  dvcs:fileentry$ $free
    dvcs:hash$ $free
    dvcs:id$ $free  dvcs:oldid$ $free
    project:revision$ $free   project:chain$ $free
    project:branch$ $free  project:project$ $free
    dvcs:commits @ .dvcs:dispose-commit
    dvcs:searchs @ .dvcs:dispose-search
    dispose ;
}scope

Variable new-files[]
Variable ref-files[]
Variable del-files[]
Variable old-files[]
Variable new-file$
Variable branches[]

: clean-up ( -- )
    new-files[] $[]off  ref-files[] $[]off
    del-files[] $[]off  old-files[] $[]off
    branches[]  $[]off  new-file$ $free ;

User tmp1$
: $tmp1 ( xt -- ) tmp1$ $free  tmp1$ $exec  tmp1$ $@ ;

: mode@ ( -- mode )
    statbuf st_mode [ sizeof st_mode 2 = ] [IF] w@ [ELSE] l@ [THEN] ;
: mode! ( -- mode )
    statbuf st_mode [ sizeof st_mode 2 = ] [IF] w! [ELSE] l! [THEN] ;

$1000 Constant path-max#

Defer xstat ' lstat is xstat
Defer xfiles[] ' new-files[] is xfiles[]
Defer hash-import ' noop is hash-import

: ref-hash-import ( hash u -- hash u )
    2>r new-file$ $@ write-enc-hashed 2drop 2r> ;

: hashstat-rest ( addr u -- addr' u' )
    [: mode@ { | w^ perm } perm le-w!
	statbuf st_mtime ntime@ d>64 64#0 { 64^ timestamp } timestamp le-64!
	perm le-uw@ S_IFMT? and  case
	    S_IFLNK of  path-max# new-file$ $!len \ pathmax: 4k
		2dup new-file$ $@ readlink
		dup ?ior new-file$ $!len  endof
	    S_IFREG of  2dup new-file$ $slurp-file  endof
	    0 new-file$ $!len
	endcase
	new-file$ $@ >file-hash 2dup type hash-import
	new-file$ $@ 2swap dvcs-objects #!
	timestamp 1 64s type  perm 2 type type
    ;] $tmp1 ;

: file-hashstat ( addr u -- addr' u' )
    2dup statbuf xstat ?ior  hashstat-rest ;

: $ins[]f ( addr u array -- ) [ hash#128 dvcs:name ]L $ins[]/ drop ;

: new-files-loop ( -- )
    BEGIN  refill  WHILE  \ source type cr
	    source file-hashstat xfiles[] $ins[]f  REPEAT ;
: new-files-in ( addr u -- )
    r/o open-file dup no-file# = IF  2drop  EXIT  THEN  throw
    ['] new-files-loop execute-parsing-file ;
: do-refs ( xt -- )
    [: stat mode@ $1000 or mode! ;] is xstat
    ['] ref-files[] is xfiles[]
    ['] ref-hash-import is hash-import
    catch
    ['] lstat is xstat
    ['] new-files[] is xfiles[]
    ['] noop is hash-import
    throw ;
: ref-hashstat ( addr u -- addr' u' )
    ['] file-hashstat do-refs ;
: ref-files-in ( addr u -- )
    ['] new-files-in do-refs ;

: config>dvcs ( o:dvcs -- )
    "~+/.n2o/config" ['] project >body read-config
    project:revision$ $@ base85>$ dvcs:oldid$ $! ;
: files>dvcs ( o:dvcs -- )
    "~+/.n2o/files" filelist-in ;
: new>dvcs ( o:dvcs -- )
    "~+/.n2o/reffiles" ref-files-in
    "~+/.n2o/newfiles" new-files-in ;
: mode<> ( mode1 mode2 -- flag )
    over S_IFMT invert and over S_IFMT invert and <> >r
    S_IFMT and swap S_IFMT and dup $1000 and IF
	$-2000 and swap dup S_IFLNK = IF  drop S_IFREG  THEN
    THEN  <> r> or ;
: dvcs?modified ( o:dvcs -- )
    dvcs:files# [: >r
	r@ cell+ $@ drop hash#128 + dvcs:perm le-uw@ { perm }
	r@ $@ statbuf perm $1000 and IF  stat  ELSE  lstat  THEN
	0< IF  errno ENOENT = IF
		r> [: dup cell+ $. $. ;] $tmp1 del-files[] $ins[]f
		EXIT  THEN  -1 ?ior  THEN
	r@ cell+ $@ drop hash#128 + dvcs:timestamp le-64@
	statbuf st_mtime ntime@ d>64 64<>
	perm mode@ mode<> or  IF
	    r@ $@ hashstat-rest 2dup fn-split dvcs:files# #@
	    hash#128 umin 2swap hash#128 umin
	    str=
	    IF  2drop
	    ELSE  new-files[] $ins[]f
		r@ [: dup cell+ $. $. ;] $tmp1 old-files[] $ins[]f
	    THEN
	THEN  rdrop
    ;] #map dvcs(
    ." --- old files:" cr old-files[] ['] .file+hash $[]map
    ." +++ new files:" cr new-files[] ['] .file+hash $[]map
    ." +++ ref files:" cr ref-files[] ['] .file+hash $[]map
    ." ===" cr
    ) ;

: dvcs+in ( hash u -- )
    dvcs( ." read in: " 2dup 85type forth:cr )
    hash#128 umin dvcs-objects #@ over 0= !!wrong-hash!!
    dvcs:in-files$ $+! ;
: dvcs+out ( hash u -- )
    hash#128 umin dvcs-objects #@ over 0= !!wrong-hash!!
    dvcs:out-files$ $+! ;

: file-lsize@ ( addr u -- 64size )
    statbuf lstat ?ior statbuf st_size 64@
    mode@ S_IFMT? and S_IFDIR <> n>64 64and ;
: file-size@ ( addr u -- 64size )
    statbuf stat ?ior statbuf st_size 64@
    mode@ S_IFMT? and S_IFDIR <> n>64 64and ;

also net2o-base

: read-old-fs ( -- ) dvcs:in-files$ $free
    old-files[] [: hash#128 umin 2dup $, dvcs-read dvcs+in ;] $[]map ;
: read-del-fs ( -- )
    del-files[] [: over hash#128 dvcs:perm + le-uw@
	S_IFMT? and S_IFDIR =  IF  /name $, dvcs-rmdir
	ELSE 2dup [: over hash#128 forth:type /name forth:type ;] $tmp1 $,
	    dvcs-rm hash#128 umin dvcs+in  THEN ;] $[]map ;
: read-new-fs ( -- )
    new-files[] ['] dvcs+out $[]map ;
: dvcs+hash ( addr u -- ) 2drop ; \ !!STUB!!
: read-ref-fs ( -- )
    ref-files[] ['] dvcs+hash $[]map ;
: write-new-fs ( -- )
    new-files[] [: 2dup hash#128 dvcs:perm /string $,
	/name file-lsize@ lit, dvcs-write ;] $[]map ;
: write-ref-fs ( -- )
    ref-files[] [:
	[: over hash#128 forth:type
	    hash#128 dvcs:perm /string forth:type ;] $tmp $,
	dvcs-ref ;] $[]map ;
: compute-patch ( -- )
    dvcsfiles( ." ===== in-files$ ====" forth:cr dvcs:in-files$ $.
    ." ===== out-files$ =====" forth:cr dvcs:out-files$ $. )
    dvcs:in-files$ dvcs:out-files$ ['] bdelta$2 dvcs:patch$ $exec
    dvcs:patch$ $@ $, dvcs:out-files$ $@len ulit, dvcs-patch ;

: compute-diff ( -- )
    read-old-fs  read-del-fs  read-new-fs  read-ref-fs
    compute-patch  write-new-fs write-ref-fs ;

Variable id-files[]

: dvcs-gen-id ( -- addr u )
    id-files[] $[]off
    dvcs:files# [: dup cell+ $@ 2>r $@ 2r> [: forth:type forth:type ;] $tmp
	id-files[] $ins[] drop ;] #map \ sort filenames
    [:  id-files[] [: drop hash#128 $, dvcs-read ;] $[]map
	id-files[] [: hash#128 /string
	    0 dvcs:perm /string 2dup $,
	    2 /string file-lsize@ lit, dvcs-write
	;] $[]map ;] gen-cmd$
    dup IF  >file-hash  THEN ;

previous

: 85$! ( addr u $addr -- )
    ['] 85type swap $set ;

: save-project ( -- )
    dvcs( ." saving '" dvcs:id$ $@ 85type cr )
    dvcs:id$ $@ project:revision$ 85$!
    "~+/.n2o/config" ['] project >body write-config ;

\ init project

: dvcs-init ( project u -- )
    ".n2o" $1FF init-dir drop
    ".n2o/files" touch
    dvcs:new-dvcs >o
    '#' $split  dup 0= IF  2drop "master"  ELSE  2swap  THEN
    project:branch$ $!  project:project$ $!
    save-project  dvcs:dispose-dvcs o> ;

\ append a line

: append-line ( addr u file u -- )
    2dup w/o open-file dup no-file# = IF
	2drop w/o create-file throw  ELSE  throw nip nip  THEN
    >r r@ file-size throw r@ reposition-file throw
    r@ write-line throw r> close-file throw ;

\ patch stuff

\ read in branches, new version

: hash+type ( addr u type addr1 -- ) >r r@ $free
    [: { w^ x } type x cell type ;] r> $exec ;

' 2drop commit-class is msg:tag
' 2drop commit-class is msg:start
' 2drop commit-class is msg:coord
' 2drop commit-class is msg:signal
' 2drop commit-class is msg:text
' 2drop commit-class is msg:url
' 2drop commit-class is msg:action
' 2drop commit-class is msg:chain
' drop  commit-class is msg:like
' noop  commit-class is msg:end
' drop  commit-class is msg:redisplay

:noname ( addr u -- )
    re$ $+! ; commit-class is msg:re
:noname ( addr u -- )
    id$ $! re$ $free ; commit-class is msg:id
:noname ( addr u type -- )
    object$ hash+type
    object$ $@ key| id$ $@
    id>patch# id>snap# re$ $@len select #!
    re$ $@len IF
	re$ $@ last# cell+ $+!
    THEN ; commit-class is msg:object

\ search for a specific id

' 2drop search-class is msg:start
' 2drop search-class is msg:coord
' 2drop search-class is msg:signal
' 2drop search-class is msg:text
' 2drop search-class is msg:action
' 2drop search-class is msg:chain
' 2drop search-class is msg:re
' 2drop search-class is msg:url
' noop  search-class is msg:end
' drop  search-class is msg:like
' drop  search-class is msg:redisplay

: 3drop  2drop drop ;

:noname match:tag$ $@ str= match:flag ! ; search-class is msg:tag
:noname match:flag @ IF  match:id$ $!  ELSE  2drop  THEN ; search-class is msg:id
' 3drop search-class is msg:object

' 2drop dvcs-log-class is msg:re
' 2drop dvcs-log-class is msg:coord
' 3drop dvcs-log-class is msg:object
' noop  dvcs-log-class is msg:end
:noname dvcs-log:sig$    $! ; dvcs-log-class is msg:start
:noname dvcs-log:tag$    $! ; dvcs-log-class is msg:tag
:noname dvcs-log:id$     $! ; dvcs-log-class is msg:id
:noname dvcs-log:text$   $! ; dvcs-log-class is msg:text
:noname dvcs-log:action$ $! ; dvcs-log-class is msg:action
:noname dvcs-log:chain$  $! ; dvcs-log-class is msg:chain
:noname dvcs-log:urls[] $+[]! ; dvcs-log-class is msg:url
' drop dvcs-log-class is msg:like
' drop dvcs-log-class is msg:redisplay

: chat>dvcs ( o:dvcs -- )
    project:project$ $@ @/ 2drop load-msg ;
: .hash ( addr -- )
    [: dup $@ 85type ."  -> " cell+ $@ 85type cr ;] #map ;
: chat>branches-loop ( o:commit -- )
    msg-log@ over { log } bounds ?DO
	re$ $free  object$ $free
	I $@ ['] msg:display catch IF  ." invalid entry" cr 2drop THEN
    cell +LOOP  log free throw
    dvcs( ." === id>patch ===" cr id>patch# .hash
    ." === id>snap ===" cr id>snap# .hash ) ;
: chat>branches ( o:dvcs -- )
    project:project$ $@ @/ 2drop >group  dvcs:commits @ .chat>branches-loop ;

: >branches ( addr u -- )
    $make branches[] >back ;
User id-check# \ check hash
: id>branches-loop ( addr u -- )
    BEGIN  2dup id-check# #@ d0<> IF  2drop  EXIT  THEN
	s" !" 2over id-check# #!
	2dup id>snap# #@ 2dup d0<> IF  >branches 2drop  EXIT  THEN
	2drop id>patch# #@ 2dup d0<> WHILE
	    2dup hash#128 umin >branches
	    hash#128 safe/string  hash#128 - 2dup + >r
	    bounds U+DO  I hash#128 recurse  hash#128 +LOOP
	    r> hash#128 \ tail recursion optimization
    REPEAT  2drop ;
: id>branches ( addr u -- )
    id-check# #frees
    branches[] $[]off  dvcs:commits @ .id>branches-loop
    id-check# #frees
    dvcs( ." re:" cr branches[] [: 85type cr ;] $[]map ) ;
: branches>dvcs ( -- )
    branches[] [: dup IF
	    dvcs( ." read enc hash: " 2dup 85type cr )
	    ?read-enc-hashed  c-state off  do-cmd-loop
	    dvcs:clean-delta
	ELSE  2drop  THEN
    ;] $[]map ;

\ push out a revision

: >revision ( addr u -- )
    2dup >file-hash dvcs:hash$ $!
    dvcs( ." ===== ci '" dvcs:hash$ $@ 85type ." ' =====" cr )
    write-enc-hashed 2drop ;

: pull-readin ( -- )
    config>dvcs  chat>dvcs  chat>branches ;
: dvcs-readin ( $addr -- )
    pull-readin  $@ id>branches  branches>dvcs
    files>dvcs  new>dvcs  dvcs?modified ;

scope{ dvcs
: new-dvcs-log ( -- o )
    dvcs-log-class new >o msg-table @ token-table ! o o> ;
: clear-log ( -- )
    dvcs-log:sig$    $free  dvcs-log:tag$  $free  dvcs-log:id$    $free
    dvcs-log:action$ $free  dvcs-log:text$ $free  dvcs-log:chain$ $free
    dvcs-log:urls[] $[]free ;
: dispose-dvcs-log ( o:log -- )
    clear-log dispose ;
}scope

: display-logn ( addr u n -- )
    project:branch$ $@ { d: branch }
    dvcs:new-dvcs-log >o
    cells >r >group  msg-log@ 2dup { log u }
    dup r> - 0 max dup >r /string r> cell/ -rot bounds ?DO
	dvcs:clear-log  I $@ ['] msg:display catch
	IF  ." invalid entry" cr 2drop
	ELSE
	    branch dvcs-log:tag$ $@ str= IF
		dup 0 .r ." : [" dvcs-log:id$ $@ 85type ." ] "
		dvcs-log:sig$ $@ 2dup startdate@ .ticks
		dvcs-log:chain$ $@ dup IF
		    2dup sighash? IF  <info>  ELSE  <err>  THEN
		    ." <-" drop le-64@ .ticks  <default>
		ELSE  2drop  THEN  space
		dvcs-log:action$ $. ." : " dvcs-log:text$ $. space
		dvcs-log:urls[] [: <warn> type <default> space ;] $[]map
		.key-id?
		cr
	    THEN
	THEN  1+
    cell +LOOP  drop
    log free dvcs:dispose-dvcs-log o> throw ;

: .dvcs-log ( -- )
    dvcs:new-dvcs >o  config>dvcs
    project:project$ $@ @/ 2drop 2dup load-msg
    config:logsize# @ display-logn
    dvcs:dispose-dvcs o> ;

also net2o-base
true Value add-object?

: (dvcs-newsentry) ( type -- )
    dvcs:type !
    dvcs:hash$ $@len 0= IF  #0. last-signed 2!  EXIT  THEN
    msg-group$ @ >r
    project:project$ @ msg-group$ !
    o [: with dvcs
	project:chain$ $@ base85>$
	fileref[]
	message$   $@
	type       @
	hash$      $@
	oldid$     $@
	id$        $@
	project:branch$ $@
	endwith
	$, msg-tag
	$, msg-id
	dup >r
	dup IF  $, msg-re     ELSE  2drop  THEN
	dup >r dup IF  $, ulit, msg-object
	ELSE  2drop drop  THEN
	r> r> IF   IF  "Patchset"  ELSE  "Revert"  THEN
	ELSE  drop "Snapshot"  THEN  $, msg-action
	$, msg-text
	dup [: [: ." file:" forth:type ;] $tmp $, msg-url ;] $[]map $[]free
	dup IF  $, msg-chain  ELSE  2drop  THEN
    ;] (send-avalanche) IF  .chat  ELSE   2drop  THEN
    r> msg-group$ ! ;
previous

: dvcs-snapentry ( -- )
    dvcs:oldid$ $free
    msg:snapshot# (dvcs-newsentry) ;
: dvcs-newsentry ( -- )
    msg:patch# msg:snapshot# dvcs:oldid$ $@len select (dvcs-newsentry) ;

: >id-revision ( addr u -- )
    dvcs-gen-id 2dup dvcs:id$ $!
    dvcs:commits @ >o
    2dup id>patch# #@ d0= >r id>snap# #@ d0= r> and o>
    IF  >revision  ELSE  >file-hash dvcs:hash$ $!  THEN ;

: ?delete-file ( addr u -- )
    delete-file dup no-file# <> and throw ;

: dvcs-sig$ ( -- )
    last-signed 2@ sigdate@ over 1 64s 85type 2 64s /string
    c:0key >hash hashtmp hash#128 85type ;
 
: (dvcs-ci) ( addr u o:dvcs -- ) dvcs:message$ $!
    dvcs:oldid$ dvcs-readin
    ref-files[] $[]# new-files[] $[]# del-files[] $[]# or or 0= IF
	." Nothing to do" cr
    ELSE
	['] compute-diff gen-cmd$
	$make { w^ diff$ } diff$ $@ \ 2dup net2o:see
	dvcs( ." ===== patch len: " dvcs:patch$ $@len . ." =====" cr )
	del-files[] ['] -fileentry $[]map
	new-files[] ['] +fileentry $[]map
	ref-files[] ['] +fileentry $[]map
	>id-revision  my-key >r
	my-key-default to my-key  dvcs-newsentry
	r> to my-key
	['] dvcs-sig$ project:chain$ $set
	save-project  filelist-out
	"~+/.n2o/newfiles" ?delete-file
	"~+/.n2o/reffiles" ?delete-file
	diff$ $free
    THEN  clean-up ;

: dvcs-ci ( addr u -- ) \ checkin command
    dvcs:new-dvcs >o now>never (dvcs-ci)  dvcs:dispose-dvcs o> ;

: dvcs-diff ( -- )
    dvcs:new-dvcs >o dvcs:oldid$ dvcs-readin
    ['] compute-diff gen-cmd$ 2drop
    dvcs( ." ===== diff len: " dvcs:patch$ $@len . ." =====" cr )
    dvcs:in-files$ dvcs:patch$ color-bpatch$2
    clean-up  dvcs:dispose-dvcs o> ;

: ci-args ( -- message u )
    ?nextarg 0= IF "untitled checkin" THEN
    2dup "-m" str= IF  ?nextarg IF  2nip  THEN  THEN ;

: dvcs-add ( addr u -- )
    2dup dirname dup 0<> + dup IF  recurse  ELSE  2drop  THEN
    2dup dvcs:files# #@ drop IF  2drop  EXIT
    ELSE
	\ "dummy128dummy128dummy128dummy128" 2over dvcs:files# #!
	"~+/.n2o/newfiles" append-line  THEN ;

: dvcs-ref ( addr u -- )
    2dup dirname dup 0<> + dup IF  dvcs-add  ELSE  2drop  THEN
    2dup dvcs:files# #@ drop IF  2drop  EXIT
    ELSE  "~+/.n2o/reffiles" append-line  THEN ;

: dvcs-snap ( addr u -- )
    dvcs:new-dvcs >o  dvcs:message$ $!
    config>dvcs  files>dvcs
    dvcs:files# [:
	dup cell+ $@ drop hash#128 + dvcs:perm le-uw@ $1000 and
	IF    $@  ref-hashstat ref-files[]
	ELSE  $@ file-hashstat new-files[]  THEN
	$ins[]f ;] #map
    ['] compute-diff gen-cmd$ >id-revision
    now>never  dvcs-snapentry
    save-project  clean-up dvcs:dispose-dvcs o> ;

: del-oldfile ( hash-entry -- )
    dup cell+ $@ drop hash#128 dvcs:perm + le-uw@
    S_IFMT? and S_IFDIR = IF
	$@ dvcs( ." rd " 2dup type cr ) dvcs:rmdirs[] $ins[] drop
    ELSE  dup $@ dvcs( ." rm " 2dup type cr )
	delete-file dup 0< IF
	    <warn> >r ." can't delete file " $. ." : "
	    r> error$ type <default> cr
	ELSE  2drop  THEN
    THEN ;

: new->old ( -- ) dvcs( ." === remove old files ===" cr )
    dvcs:rmdirs[] $[]off
    dvcs:oldfiles# [: dup $@ dvcs:files# #@ drop 0= IF
	    del-oldfile
	ELSE  dup cell+ $@ last# cell+ $@ str= 0= IF
		del-oldfile  THEN
	THEN ;] #map
    dvcs:rmdirs[] $@ bounds cell- swap cell- U-DO
	I $@ rmdir 0< IF
	    errno strerror <warn>
	    ." can't delete directory " I $. ." : " type <default> cr
	THEN
    cell -LOOP
    dvcs:rmdirs[] $[]off ;
: old->new ( -- ) dvcs( ." === write out new files ===" cr )
    dvcs:outfiles[] $[]off
    dvcs:files# [: $@ dvcs:outfiles[] $ins[] drop ;] #map \ sort filenames
    dvcs:outfiles[] [: dvcs:files# #@ d0<> IF
	    last# dup >r $@ dvcs:oldfiles# #@ over IF
		r@ cell+ $@ str=
	    ELSE  drop
	    THEN  0= IF
		dvcs( ." out " r@ $. space r@ cell+ $@ 85type cr )
		r@ cell+ $@ r@ $@ dvcs-outfile-name
	    THEN  rdrop  THEN ;] $[]map
    dvcs:outfiles[] $[]off ;

: co-rest ( -- )
    0 dvcs:files# !@ dvcs:oldfiles# !
    branches>dvcs  new->old  old->new
    save-project  filelist-out ;

: dvcs-co ( addr u -- ) \ checkout revision
    base85>$  dvcs:new-dvcs >o
    config>dvcs   dvcs:id$ $! dvcs:id$  dvcs-readin  co-rest
    dvcs:dispose-dvcs o> ;

: chat>searchs-loop ( o:commit -- )
    msg-log@ over { log } bounds ?DO
	I $@ ['] msg:display catch IF  ." invalid entry" cr 2drop THEN
    cell +LOOP  log free throw ;
: search-last-rev ( -- addr u )
    project:project$ $@ @/ 2drop >group
    project:branch$ $@
    dvcs:searchs @ >o match:tag$ $!
    chat>searchs-loop match:id$ $@ o> ;

: dvcs-up ( -- ) \ checkout latest revision
    dvcs:new-dvcs >o
    pull-readin  files>dvcs  new>dvcs  dvcs?modified
    new-files[] $[]# del-files[] $[]# d0= IF
	search-last-rev  2dup dvcs:id$ $!
	2dup dvcs:oldid$ $@ str= IF
	    2drop ." already up to date" cr
	ELSE  id>branches  co-rest  THEN
    ELSE
	." Local changes, don't update" cr
    THEN
    dvcs:dispose-dvcs o> ;

: dvcs-revert ( -- ) \ restore to last revision
    dvcs:new-dvcs >o
    pull-readin  dvcs:oldid$ $@  2dup dvcs:id$ $!
    id>branches  co-rest
    dvcs:dispose-dvcs o> ;

hash#128 buffer: hash-save

: hash-in ( addr u -- hash u )
    2dup >file-hash hash-save hash#128 smove
    write-enc-hashed 2drop
    hash-save hash#128 ;
: hash-add ( addr u -- )
    slurp-file hash-in 2drop ;
: hash-out ( addr u -- )
    base85>$ 2dup 2>r read-enc-hashed patch-in$ $@ 2r> hash-85 spit-file ;

\ pull and sync a database

$B $E 2Value dvcs-bufs#

Variable dvcs-request#
Variable sync-file-list[]
$10 Constant /sync-files
$20 /sync-files * Constant /sync-reqs

: dvcs-sync-none ( -- )
    -1 dvcs-request# +!@ 0<= IF  dvcs-request# off  THEN ;

event: :>dvcs-sync-done ( o -- ) >o
    file-reg# off  file-count off
    msg-group$ $@ >group ?save-msg  0 dvcs-request# !
    msg( ." === metadata sync done ===" forth:cr ) o> ;

: dvcs-sync-done ( -- )
    msg( ." dvcs-sync-done" forth:cr )
    net2o:close-all
    msg( ." dvcs-sync-done closed" forth:cr )
    <event o elit, :>dvcs-sync-done wait-task @ event> ;

: +dvcs-sync-done ( -- )
    ['] dvcs-sync-done is sync-done-xt
    ['] dvcs-sync-none is sync-none-xt ;

also net2o-base
: dvcs-join, ( -- )
    [: msg-join last?, ;] [msg,] ;
previous

: dvcs-greet ( -- )
    net2o-code expect-msg
    log !time end-with dvcs-join, get-ip end-code ;

: dvcs-connect ( addr u -- )
    dvcs-bufs# chat#-connect? IF  2 dvcs-request# !  dvcs-greet  THEN ;

: dvcs-connect-key ( addr u -- )
    key>group ?load-msgn
    2dup search-connect ?dup-IF  >o +group rdrop 2drop  EXIT  THEN
    \ check for disconnected here or in pk-peek?
    2dup pk-peek?  IF  dvcs-connect  ELSE  2drop  THEN ;

: dvcs-connects? ( -- flag )
    chat-keys ['] dvcs-connect-key $[]map dvcs-request# @ 0> ;

: wait-dvcs-request ( -- )
    BEGIN  dvcs-request# @  WHILE  stop  REPEAT ;

: +needed ( addr u -- )
    2dup enchash>filename file-status nip no-file# = IF
	dvcs( ." need: " 2dup 85type cr )
	sync-file-list[] $ins[] drop
    ELSE  dvcs( ." don't need: " 2dup 85type cr ) 2drop  THEN ;

: #needed ( hash -- )
    cell+ $@ key| +needed ;
: dvcs-needed-files ( -- )
    id>patch# ['] #needed #map
    id>snap#  ['] #needed #map ;

: get-needed-files { list -- } +resend
    list $[]# 0 ?DO
	I /sync-reqs + I' umin I U+DO
	    net2o-code expect+slurp
	    $10 blocksize! 0 blockalign!
	    I /sync-files + I' umin I U+DO
		I list $[]@ net2o:copy#
	    LOOP
\	    I /sync-files + I' u>= IF
	    end-code| net2o:close-all -map-resend
	    I /sync-files + I' u>= IF
		+resend +flow-control
		net2o-code expect+slurp  close-all  ack rewind end-with
		[ previous ]
	    THEN
\	    ELSE  end-code  THEN
	/sync-files +LOOP
    /sync-reqs +LOOP ;

: dvcs-data-sync ( -- )
    sync-file-list[] $[]off  branches[] $[]off
    msg-group$ $@ >group
    dvcs:commits @ .chat>branches-loop
    dvcs:commits @ .dvcs-needed-files
    sync-file-list[] $[]# 0> connection and
    IF    sync-file-list[] connection .get-needed-files  THEN ;

: dvcs-ref-sync ( -- )
    search-last-rev id>branches
    dvcs:new-dvcs-refs >o
    branches>dvcs
    dvcs:refs[] $[]# 0 ?DO
	." ref: " I dvcs:refs[] $[]@ 85type cr  LOOP
    dvcs:refs[] $[]# 0> connection and
    IF  dvcs:refs[] connection .get-needed-files  THEN
    dvcs:dispose-dvcs-refs o> ;

: handle-fetch ( -- )  ?.net2o/objects
    dvcs:new-dvcs >o  pull-readin
    msg( ." === syncing metadata ===" forth:cr )
    0 >o dvcs-connects? IF  +dvcs-sync-done  wait-dvcs-request  THEN o>
    msg( ." === syncing data ===" forth:cr )
    dvcs-data-sync
    msg( ." === data sync done ===" forth:cr )
    dvcs-ref-sync
    msg( ." === ref sync done ===" forth:cr )
    connection ?dup-IF
	.data-rmap IF  msg-group$ $@ >group last# silent-leave-chat  THEN
    THEN
    dvcs:dispose-dvcs o> ;

: handle-clone ( -- )
    0 chat-keys !@ { w^ clone-keys }
    clone-keys [: >dir  2dup chat-keys $+[]!
	[: @/ 2swap
	    '#' $split dup 0= IF  2drop  ELSE  2nip  THEN
	    2dup $1FF init-dir drop 2dup set-dir throw
	    [: type '@' emit .key-id? ;] $tmp dvcs-init
	    handle-fetch dvcs-up ;] catch
	chat-keys $[]free  dir>  throw
    ;] $[]map
    clone-keys @ chat-keys ! ;

\\\
Local Variables:
forth-local-words:
    (
     (("net2o:" "+net2o:") definition-starter (font-lock-keyword-face . 1)
      "[ \t\n]" t name (font-lock-function-name-face . 3))
     ("[a-z0-9]+(" immediate (font-lock-comment-face . 1)
      ")" nil comment (font-lock-comment-face . 1))
    )
forth-local-indent-words:
    (
     (("net2o:" "+net2o:") (0 . 2) (0 . 2) non-immediate)
    )
End:
