I was pretty bored, so with the help of copy paste and then rewriting from ocaml-git and tiny-git I've managed to create a dumb version control in ocaml.
It has commits of course, a way to see the log and diffs so far.
Tony is too slow with his fossil video and fossilhub, so here I am.

ml
(*
* GPL header for vc.ml - Part of vc
*
* Copyright (C) 2026 Alice
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* vc.ml
*)
(*
* For normal build: ocamlfind ocamlopt -linkpkg -package unix vc.ml -o vc
* For static build: ocamlfind ocamlopt -linkpkg -package unix -ccopt -static vc.ml -o vc
*)
open Unix;;
let vc_file = ".vc";;
let read_file filename =
let ic = open_in_bin filename in
let len = in_channel_length ic in
let buf = Bytes.create len in
really_input ic buf 0 len;
close_in ic;
Bytes.to_string buf
;;
let write_file filename content =
let oc = open_out_bin filename in
output_string oc content;
close_out oc
;;
let load_history () =
try let ic = open_in_bin vc_file in
let h = (Marshal.from_channel ic : (string * string) list) in
close_in ic; h
with _ -> []
;;
let save msg =
let history = load_history () in
ignore (Sys.command
"tar \
--exclude=.vc \
--exclude=vc \
--exclude=\"*.obj\" \
--exclude=\"*.so\" \
--exclude=\"*.s\" \
--exclude=\"*.o\" \
-cf /tmp/vc.tar . 2>/dev/null"
);
let tar_data = read_file "/tmp/vc.tar" in
Sys.remove "/tmp/vc.tar";
let oc = open_out_bin vc_file in
Marshal.to_channel oc ((msg, tar_data) :: history) [];
close_out oc;
Printf.printf "Ver %d\n" (List.length history)
;;
let browse () =
let history = load_history () in
List.iteri (fun i (msg, _) -> Printf.printf " %d: %s\n" i msg)
(List.rev history)
;;
let load idx =
let history = load_history () in
match List.nth_opt (List.rev history) idx with
| Some (_, tar) ->
write_file "/tmp/vc.tar" tar;
ignore (Sys.command
"find . -maxdepth 1 \
-type f \
-not -name '.vc' \
-not -name 'vc' \
-delete; \
tar -xf /tmp/vc.tar; \
rm /tmp/vc.tar"
);
Printf.printf "Rev to ver: %d\n" idx
| None -> Printf.printf "No ver `%d`\n" idx
;;
let diff_ver idx =
let history = load_history () in
match List.nth_opt (List.rev history) idx with
| Some (msg, tar) -> Printf.printf "Diff with Ver %d: %s\n%!" idx msg;
let cmd = "sh -c ' \
DIR=/tmp/vc_diff_$$\x20; \
mkdir -p \"$DIR\" && \
tar -xf - -C \"$DIR\" && \
diff -r \
--exclude=.vc \
--exclude=vc \
--exclude=\"*.obj\" \
--exclude=\"*.so\" \
--exclude=\"*.s\" \
--exclude=\"*.o\" \
\"$DIR\" . | grep -v \"^diff -r\" ;\
STATUS=${PIPESTATUS[0]} ; \
rm -rf \"$DIR\" ; \
exit $STATUS'"
in
let oc = Unix.open_process_out cmd in
output_string oc tar;
ignore (Unix.close_process_out oc)
| None -> Printf.printf "No ver `%d` to diff against\n" idx
;;
let () =
match Sys.argv with
| [| _; "put"; msg |] -> save msg
| [| _; "see" |] -> browse ()
| [| _; "get"; idx |] -> (match int_of_string_opt idx with Some i -> load i | None -> ())
| [| _; "diff"; idx |] -> (match int_of_string_opt idx with Some i -> diff_ver i | None -> ())
| _ -> print_endline "\
Usage:
vc put \"MSG\" - Save state of dir with msg
vc see - Peak at saved sates and get `ID` and msg
vc get `ID` - Revert to state ID
vc diff `ID` - Diff compared to ID state"
;;
clean and minimal, but it has two critical downsides (in my opinion):
vc get destroys the work tree completely. ideally a version control system shouldn't overwrite files that are not registered in it
- rather than saving diffs, you're saving the entire state of the work tree. this might get problematic with big codebases
but a fun start nonetheless
I have no idea on how to do it in diffs, but i do wish to do it
I also plan on having a small server machine running i can connect to for stuff like this
A simple way could for now be save 3 states, or just ignore the problem
And the vc get problem that it removes new files can be either a feature or fixed by unpacking vc get into a tmp dir and moving i to root dir
the goal of course is to use it for my work stuff and small projects
fixed the remove files that are newer, i actually had the delete other fdlag....

This is a lot better
It does not overwrite non tracked files when i do vc get ID and it has username and timestamp of the puter
This is an improved version
(*
* GPL header for vc.ml - Part of vc
*
* Copyright (C) 2026 Alice
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* vc.ml
*)
(*
******************************************************************************************
* *
* Dependencies: *
* - diff *
* - tar *
* - sh *
* *
******************************************************************************************
* *
* For normal build: ocamlfind ocamlopt -linkpkg -package unix vc.ml -o vc *
* For static build: ocamlfind ocamlopt -linkpkg -package unix -ccopt -static vc.ml -o vc *
* *
******************************************************************************************
*)
open Unix;;
let vc_file = ".vc";;
let read_file filename =
let ic = open_in_bin filename in
let len = in_channel_length ic in
let buf = Bytes.create len in
really_input ic buf 0 len;
close_in ic;
Bytes.to_string buf
;;
let write_file filename content =
let oc = open_out_bin filename in
output_string oc content;
close_out oc
;;
let load_history () =
try let ic = open_in_bin vc_file in
let h = (Marshal.from_channel ic : (string * string * string * string) list) in
close_in ic; h
with _ -> []
;;
let save msg =
let history = load_history () in
let user = try Unix.getlogin () with _ -> "unknown" in
let tm = Unix.localtime (Unix.time ()) in
let timestamp = Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d"
(tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday
tm.tm_hour tm.tm_min tm.tm_sec in
let cmd = "tar --exclude=.vc --exclude=vc --exclude=\"*.obj\" \
--exclude=\"*.so\" --exclude=\"*.s\" --exclude=\"*.o\" \
-cf - . 2>/dev/null" in
let ic = Unix.open_process_in cmd in
let len = 65536 in
let buf = Buffer.create len in
let bytes = Bytes.create len in
let rec read () =
let n = input ic bytes 0 len in
if n > 0 then (Buffer.add_subbytes buf bytes 0 n; read ()) in
read ();
ignore (Unix.close_process_in ic);
let tar_data = Buffer.contents buf in
let oc = open_out_bin vc_file in
Marshal.to_channel oc ((msg, user, timestamp, tar_data) :: history) [];
close_out oc;
Printf.printf "Ver: %d\nBy: %s \nAt: %s\nMsg: %s\n" (List.length history) user timestamp msg
;;
let see () =
let history = load_history () in
List.iteri (fun i (msg, user, timestamp, _) ->
Printf.printf "\t%d: [%s] By: %s | %s\n" i timestamp user msg)
(List.rev history)
;;
let load idx =
let history = load_history () in
match List.nth_opt (List.rev history) idx with
| Some (_, _, _, tar) -> let oc = Unix.open_process_out "tar -xf -" in
output_string oc tar;
ignore (Unix.close_process_out oc);
Printf.printf "\nRev to ver: %d\n" idx
| None -> Printf.printf "No ver `%d`\n" idx
;;
let diff_ver idx =
let history = load_history () in
match List.nth_opt (List.rev history) idx with
| Some (msg, user, timestamp, tar) ->
Printf.printf "\nDiff with Ver %d (by %s on %s): %s\n\n%!" idx user timestamp msg;
let cmd = "sh -c ' \
DIR=/tmp/vc_diff_$$\x20; \
mkdir -p \"$DIR\" && \
tar -xf - -C \"$DIR\" && \
diff -r \
--exclude=.vc \
--exclude=vc \
--exclude=\"*.obj\" \
--exclude=\"*.so\" \
--exclude=\"*.s\" \
--exclude=\"*.o\" \
\"$DIR\" . | grep -v \"^diff -r\" ;\
STATUS=${PIPESTATUS[0]} ; \
rm -rf \"$DIR\" ; \
exit $STATUS'"
in
let oc = Unix.open_process_out cmd in
output_string oc tar;
ignore (Unix.close_process_out oc)
| None -> Printf.printf "No ver `%d` to diff against\n" idx
;;
let () =
match Sys.argv with
| [| _; "put"; msg |] -> save msg
| [| _; "see" |] -> see ()
| [| _; "get"; idx |] -> (match int_of_string_opt idx with Some i -> load i | None -> ())
| [| _; "diff"; idx |] -> (match int_of_string_opt idx with Some i -> diff_ver i | None -> ())
| _ -> print_endline "\
Usage:
vc put \"MSG\" - Save state of dir with msg
vc see - Peak at saved sates and get `ID` and msg
vc get `ID` - Revert to state ID
vc diff `ID` - Diff compared to ID state"
;;
as one can see its just a simple
ml
(*
* GPL header for vc.ml - Part of vc
*
* Copyright (C) 2026 Alice
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* vc.ml
*)
(*
* Dependencies:
* - diff
* - tar
* - sh
* For normal build: ocamlfind ocamlopt -linkpkg -package unix vc.ml -o vc
* For static build: ocamlfind ocamlopt -linkpkg -package unix -ccopt -static vc.ml -o vc
*)
open Unix;;
let vc_dir = ".vc";;
let snapshots_dir = Filename.concat vc_dir "vers";;
let history_file = Filename.concat vc_dir "log" ;;
let init () =
if not (Sys.file_exists vc_dir) then Unix.mkdir vc_dir 0o755;
if not (Sys.file_exists snapshots_dir) then Unix.mkdir snapshots_dir 0o755
;;
let load_history () =
try
let ic = open_in_bin history_file in
let h = (Marshal.from_channel ic : (string * string * string) list) in
close_in ic; h
with _ -> []
;;
let save_history h =
let oc = open_out_bin history_file in
Marshal.to_channel oc h [];
close_out oc
;;
let get_timestamp () =
let tm = Unix.localtime (Unix.time ()) in
Printf.sprintf "%04d-%02d-%02d %02d:%02d:%02d"
(tm.tm_year + 1900)
(tm.tm_mon + 1)
tm.tm_mday
tm.tm_hour
tm.tm_min
tm.tm_sec
;;
let save msg =
init ();
let history = load_history () in
let user = try Unix.getlogin () with _ -> "unknown" in
let timestamp = get_timestamp () in
let id = List.length history in
let tar_file = Filename.concat snapshots_dir (string_of_int id ^ ".tar") in
let cmd =
Printf.sprintf "tar --exclude=%s \
--exclude=\".vc\" \
--exclude=\"vc\" \
--exclude=\"*.obj\" \
--exclude=\"*.so\" \
--exclude=\"*.s\" \
--exclude=\"*.o\" \
-cf %s . 2>/dev/null"
vc_dir tar_file in
ignore (Sys.command cmd);
save_history ((msg, user, timestamp) :: history);
Printf.printf "\
Ver: %d\n\
By: %s\n\
At: %s\n\
Msg: %s\n"
id user timestamp msg
;;
let see () =
let history = load_history () in
List.iteri (fun i (msg, user, timestamp) ->
Printf.printf "\t%d: [%s] By: %s | %s\n" i timestamp user msg)
(List.rev history)
;;
let load idx =
let tar_file = Filename.concat snapshots_dir (string_of_int idx ^ ".tar") in
if Sys.file_exists tar_file then begin
let cmd = Printf.sprintf "tar -xf %s" tar_file in
ignore (Sys.command cmd);
Printf.printf "\nRev to ver: %d\n" idx
end else
Printf.printf "No ver `%d`\n" idx
;;
let diff_ver idx =
let tar_file = Filename.concat snapshots_dir (string_of_int idx ^ ".tar") in
if Sys.file_exists tar_file then begin
let history = load_history () in
let meta = match List.nth_opt (List.rev history) idx with
| Some (msg, user, timestamp) -> Printf.sprintf "Ver %d (by %s on %s): %s" idx user timestamp msg
| None -> Printf.sprintf "Ver %d" idx in
Printf.printf "\nDiff with %s\n\n%!" meta;
let cmd = Printf.sprintf "\
sh -c ' \
DIR=/tmp/vc_diff_$$ ; \
mkdir -p \"$DIR\" && \
tar -xf %s -C \"$DIR\" && \
diff -r --exclude=%s \
--exclude=\"*.obj\" \
--exclude=\"*.so\" \
--exclude=\"*.s\" \
--exclude=\"*.o\" \
\"$DIR\" . | grep -v \
\"^diff -r\" ; \
STATUS=${PIPESTATUS[0]} ; \
rm -rf \"$DIR\" ; \
exit $STATUS'" tar_file vc_dir
in ignore (Sys.command cmd); print_string "\n" end else
Printf.printf "No ver `%d`\n" idx
;;
let () =
match Sys.argv with
| [| _; "put"; msg |] -> save msg
| [| _; "see" |] -> see ()
| [| _; "get"; idx |] -> (match int_of_string_opt idx with Some i -> load i | None -> ())
| [| _; "diff"; idx |] -> (match int_of_string_opt idx with Some i -> diff_ver i | None -> ())
| _ -> print_endline "\
Usage:
vc put \"MSG\" - Save state of dir with msg
vc see - Peek at saved states and get `ID` and msg
vc get `ID` - Revert to state ID
vc diff `ID` - Diff compared to ID state"
;;
As it is not the best to save it all in one tarball and write, load, save from ram etc i split it and have versions.
This means that if i vc put "message save commit" it will create a tarball snapshot under the filepath .vc/vers/index.tar
This also makes it so it can be asier to share and compare to a specified version.
Log of saves is in a .vc/log file
functionallt it is the same, but it will now do one tar per save, doesnt read on save, vc see only reads a small text file and les in and out of memory, which segnificantly speeds it up.
can add a vc save command which tars up the .vc directory for portability.
https://git.catkin.dev/trondelag/misc/src/branch/rein/vc.org
For more
Will be there now, I did not use the forum as I was too lazy to use fit there (lie)
I totally did it to share
But thanks deci for that feedback
Just say if it's something someone may want and I can look at it
I have now sucsessfully added .vc/ignore, works and has same format as .gitignore