vc - the superior version control

started by alice 2026-06-07

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):

  1. vc get destroys the work tree completely. ideally a version control system shouldn't overwrite files that are not registered in it
  2. 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.


How the filetree looks

I have now sucsessfully added .vc/ignore, works and has same format as .gitignore