New Upstream Release - unison-2.51+4.13.1

Ready changes

Summary

Merged new upstream version: 2.53.3 (was: 2.51.5).

Diff

diff --git a/.depend b/.depend
deleted file mode 100644
index fb248ed..0000000
--- a/.depend
+++ /dev/null
@@ -1,209 +0,0 @@
-doc/docs.cmo :
-doc/docs.cmx :
-icons/svg_to_data.cmo :
-icons/svg_to_data.cmx :
-src/abort.cmo : src/abort.cmi
-src/abort.cmx : src/abort.cmi
-src/bytearray.cmo : src/bytearray.cmi
-src/bytearray.cmx : src/bytearray.cmi
-src/case.cmo : src/case.cmi
-src/case.cmx : src/case.cmi
-src/checksum.cmo : src/checksum.cmi
-src/checksum.cmx : src/checksum.cmi
-src/clroot.cmo : src/clroot.cmi
-src/clroot.cmx : src/clroot.cmi
-src/common.cmo : src/common.cmi
-src/common.cmx : src/common.cmi
-src/copy.cmo : src/copy.cmi
-src/copy.cmx : src/copy.cmi
-src/external.cmo : src/external.cmi
-src/external.cmx : src/external.cmi
-src/fileinfo.cmo : src/fileinfo.cmi
-src/fileinfo.cmx : src/fileinfo.cmi
-src/files.cmo : src/files.cmi
-src/files.cmx : src/files.cmi
-src/fileutil.cmo : src/fileutil.cmi
-src/fileutil.cmx : src/fileutil.cmi
-src/fingerprint.cmo : src/fingerprint.cmi
-src/fingerprint.cmx : src/fingerprint.cmi
-src/fpcache.cmo : src/fpcache.cmi
-src/fpcache.cmx : src/fpcache.cmi
-src/fs.cmo : src/fs.cmi
-src/fs.cmx : src/fs.cmi
-src/fspath.cmo : src/fspath.cmi
-src/fspath.cmx : src/fspath.cmi
-src/fswatch.cmo : src/fswatch.cmi
-src/fswatch.cmx : src/fswatch.cmi
-src/fswatchold.cmo : src/fswatchold.cmi
-src/fswatchold.cmx : src/fswatchold.cmi
-src/globals.cmo : src/globals.cmi
-src/globals.cmx : src/globals.cmi
-src/linkgtk.cmo :
-src/linkgtk.cmx :
-src/linkgtk2.cmo :
-src/linkgtk2.cmx :
-src/linktext.cmo :
-src/linktext.cmx :
-src/lock.cmo : src/lock.cmi
-src/lock.cmx : src/lock.cmi
-src/main.cmo :
-src/main.cmx :
-src/mkProjectInfo.cmo :
-src/mkProjectInfo.cmx :
-src/name.cmo : src/name.cmi
-src/name.cmx : src/name.cmi
-src/os.cmo : src/os.cmi
-src/os.cmx : src/os.cmi
-src/osx.cmo : src/osx.cmi
-src/osx.cmx : src/osx.cmi
-src/path.cmo : src/path.cmi
-src/path.cmx : src/path.cmi
-src/pixmaps.cmo :
-src/pixmaps.cmx :
-src/pred.cmo : src/pred.cmi
-src/pred.cmx : src/pred.cmi
-src/props.cmo : src/props.cmi
-src/props.cmx : src/props.cmi
-src/recon.cmo : src/recon.cmi
-src/recon.cmx : src/recon.cmi
-src/remote.cmo : src/remote.cmi
-src/remote.cmx : src/remote.cmi
-src/sortri.cmo : src/sortri.cmi
-src/sortri.cmx : src/sortri.cmi
-src/stasher.cmo : src/stasher.cmi
-src/stasher.cmx : src/stasher.cmi
-src/strings.cmo : src/strings.cmi
-src/strings.cmx : src/strings.cmi
-src/system.cmo : src/system.cmi
-src/system.cmx : src/system.cmi
-src/terminal.cmo : src/terminal.cmi
-src/terminal.cmx : src/terminal.cmi
-src/test.cmo : src/test.cmi
-src/test.cmx : src/test.cmi
-src/transfer.cmo : src/transfer.cmi
-src/transfer.cmx : src/transfer.cmi
-src/transport.cmo : src/transport.cmi
-src/transport.cmx : src/transport.cmi
-src/tree.cmo : src/tree.cmi
-src/tree.cmx : src/tree.cmi
-src/uicommon.cmo : src/uicommon.cmi
-src/uicommon.cmx : src/uicommon.cmi
-src/uigtk2.cmo : src/uigtk2.cmi
-src/uigtk2.cmx : src/uigtk2.cmi
-src/uimacbridge.cmo :
-src/uimacbridge.cmx :
-src/uimacbridgenew.cmo :
-src/uimacbridgenew.cmx :
-src/uitext.cmo : src/uitext.cmi
-src/uitext.cmx : src/uitext.cmi
-src/unicode.cmo : src/unicode.cmi
-src/unicode.cmx : src/unicode.cmi
-src/unicode_tables.cmo :
-src/unicode_tables.cmx :
-src/update.cmo : src/update.cmi
-src/update.cmx : src/update.cmi
-src/uutil.cmo : src/uutil.cmi
-src/uutil.cmx : src/uutil.cmi
-src/xferhint.cmo : src/xferhint.cmi
-src/xferhint.cmx : src/xferhint.cmi
-tools/ask.cmo :
-tools/ask.cmx :
-tools/asktk.cmo :
-tools/asktk.cmx :
-unicode_utils/reorder.cmo :
-unicode_utils/reorder.cmx :
-unicode_utils/unicode.cmo :
-unicode_utils/unicode.cmx :
-unicode_utils/unicode_build.cmo :
-unicode_utils/unicode_build.cmx :
-unicode_utils/unicode_test.cmo :
-unicode_utils/unicode_test.cmx :
-src/abort.cmi :
-src/bytearray.cmi :
-src/case.cmi :
-src/checksum.cmi :
-src/clroot.cmi :
-src/common.cmi :
-src/copy.cmi :
-src/external.cmi :
-src/fileinfo.cmi :
-src/files.cmi :
-src/fileutil.cmi :
-src/fingerprint.cmi :
-src/fpcache.cmi :
-src/fs.cmi :
-src/fspath.cmi :
-src/fswatch.cmi :
-src/fswatchold.cmi :
-src/globals.cmi :
-src/lock.cmi :
-src/name.cmi :
-src/os.cmi :
-src/osx.cmi :
-src/path.cmi :
-src/pred.cmi :
-src/props.cmi :
-src/recon.cmi :
-src/remote.cmi :
-src/sortri.cmi :
-src/stasher.cmi :
-src/strings.cmi :
-src/system.cmi :
-src/terminal.cmi :
-src/test.cmi :
-src/transfer.cmi :
-src/transport.cmi :
-src/tree.cmi :
-src/ui.cmi :
-src/uicommon.cmi :
-src/uigtk2.cmi :
-src/uitext.cmi :
-src/unicode.cmi :
-src/update.cmi :
-src/uutil.cmi :
-src/xferhint.cmi :
-src/fsmonitor/watchercommon.cmo : src/fsmonitor/watchercommon.cmi
-src/fsmonitor/watchercommon.cmx : src/fsmonitor/watchercommon.cmi
-src/lwt/lwt.cmo : src/lwt/lwt.cmi
-src/lwt/lwt.cmx : src/lwt/lwt.cmi
-src/lwt/lwt_unix.cmo : src/lwt/lwt_unix.cmi
-src/lwt/lwt_unix.cmx : src/lwt/lwt_unix.cmi
-src/lwt/lwt_util.cmo : src/lwt/lwt_util.cmi
-src/lwt/lwt_util.cmx : src/lwt/lwt_util.cmi
-src/lwt/pqueue.cmo : src/lwt/pqueue.cmi
-src/lwt/pqueue.cmx : src/lwt/pqueue.cmi
-src/system/system_generic.cmo :
-src/system/system_generic.cmx :
-src/system/system_intf.cmo :
-src/system/system_intf.cmx :
-src/system/system_win.cmo :
-src/system/system_win.cmx :
-src/ubase/myMap.cmo : src/ubase/myMap.cmi
-src/ubase/myMap.cmx : src/ubase/myMap.cmi
-src/ubase/prefs.cmo : src/ubase/prefs.cmi
-src/ubase/prefs.cmx : src/ubase/prefs.cmi
-src/ubase/proplist.cmo : src/ubase/proplist.cmi
-src/ubase/proplist.cmx : src/ubase/proplist.cmi
-src/ubase/rx.cmo : src/ubase/rx.cmi
-src/ubase/rx.cmx : src/ubase/rx.cmi
-src/ubase/safelist.cmo : src/ubase/safelist.cmi
-src/ubase/safelist.cmx : src/ubase/safelist.cmi
-src/ubase/trace.cmo : src/ubase/trace.cmi
-src/ubase/trace.cmx : src/ubase/trace.cmi
-src/ubase/uarg.cmo : src/ubase/uarg.cmi
-src/ubase/uarg.cmx : src/ubase/uarg.cmi
-src/ubase/util.cmo : src/ubase/util.cmi
-src/ubase/util.cmx : src/ubase/util.cmi
-src/fsmonitor/watchercommon.cmi :
-src/lwt/lwt.cmi :
-src/lwt/lwt_unix.cmi :
-src/lwt/lwt_util.cmi :
-src/lwt/pqueue.cmi :
-src/ubase/myMap.cmi :
-src/ubase/prefs.cmi :
-src/ubase/proplist.cmi :
-src/ubase/rx.cmi :
-src/ubase/safelist.cmi :
-src/ubase/trace.cmi :
-src/ubase/uarg.cmi :
-src/ubase/util.cmi :
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..6db1c59
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,1220 @@
+name: CI
+
+env:
+  PROJECT_NAME: unison
+  PROJECT_DESC: "`unison` file synchronizer"
+  PROJECT_EXES: "unison unison-fsmonitor"
+
+on:
+  - pull_request
+  - push
+
+jobs:
+  docs:
+    runs-on: ubuntu-20.04
+
+    steps:
+    - run: sudo apt-get update
+
+    - name: Checkout code
+      uses: actions/checkout@v3
+
+    - name: Use OCaml
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: 4.14.x
+        opam-depext: false
+
+    - run: sudo apt-get install hevea lynx texlive-latex-base
+
+    - run: opam exec -- make docs
+
+    - name: Store user manual for the build jobs
+      uses: actions/upload-artifact@v3
+      with:
+        name: unison-docs
+        path: |
+          doc/unison-manual.txt
+          doc/unison-manual.html
+          doc/unison-manual.pdf
+
+  build:
+    if: ${{ !cancelled() }}    # Don't fail if 'docs' failed
+    needs: docs
+
+    strategy:
+      fail-fast: false
+      matrix:
+        job:
+        - { os: macos-12      , ocaml-version: 5.0.0 }
+        - { os: macos-12      , ocaml-version: 4.14.1 }
+        - { os: macos-10.15   , ocaml-version: 4.14.1           , publish: true  , fnsuffix: -macos-x86_64 }
+        - { os: ubuntu-22.04  , ocaml-version: 5.0.0 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.14.1 }
+        - { os: ubuntu-20.04  , ocaml-version: 4.14.1 }
+        - { os: windows-2022  , ocaml-version: 4.14.0+mingw64c  , publish: true  , fnsuffix: -windows-x86_64 }
+        - { os: windows-2019  , ocaml-version: 4.14.0+mingw32c  , publish: true  , fnsuffix: -windows-i386 }
+
+    runs-on: ${{ matrix.job.os }}
+
+    steps:
+    - if: contains(matrix.job.os, 'ubuntu')
+      run: sudo apt-get update
+
+    - if: runner.os == 'Windows'
+      name: "Windows: Stash away the default MSYS installation"
+      continue-on-error: true
+      shell: cmd
+      # This conflicts with Cygwin installed by setup-ocaml
+      # Adjusting PATH alone does not seem to work
+      run: rename C:\msys64 dmsys64
+
+    - name: Checkout code
+      uses: actions/checkout@v3
+
+    - name: Initialize workflow variables
+      id: vars
+      shell: bash
+      run: |
+        outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT ; done; }
+        # normalize to pre-compiled ocaml compiler variants for windows/Cygwin (decreases OCaml install time by 50%)
+        case '${{ matrix.job.ocaml-version }}' in
+          *+*) OCAML_COMPILER='ocaml-variants.${{ matrix.job.ocaml-version }}' ;;
+            *) OCAML_COMPILER='ocaml-base-compiler.${{ matrix.job.ocaml-version }}' ;;
+        esac
+        OCAML_VARIANT='${{ matrix.job.ocaml-version }}'
+        OCAML_VARIANT="${OCAML_VARIANT/+options/}"
+        outputs OCAML_VARIANT OCAML_COMPILER
+        # architecture/platform vars
+        EXE_suffix='' ; case '${{ matrix.job.os }}' in windows-*) EXE_suffix=".exe" ;; esac
+        MinGW_ARCH='x86_64' ; case '${{ matrix.job.ocaml-version }}' in *+mingw32*) MinGW_ARCH='i686' ;; *+mingw64*) MinGW_ARCH='x86_64' ;; esac
+        MSVC_ARCH='' ; case '${{ matrix.job.ocaml-version }}' in *+msvc32*) MSVC_ARCH='x86' ;; *+msvc64*) MSVC_ARCH='x64' ;; esac
+        STATIC='false' ; case '${{ matrix.job.ocaml-version }}' in *-musl*) STATIC='true' ;; esac
+        outputs EXE_suffix MinGW_ARCH MSVC_ARCH STATIC
+        case '${{ matrix.job.os }}','${{ matrix.job.ocaml-version }}' in
+          macos-*,4*) MACOSX_DEPLOYMENT_TARGET=10.6 ;; macos-*,5*) MACOSX_DEPLOYMENT_TARGET=10.7 ;;
+        esac
+        case '${{ matrix.job.os }}' in
+          macos-*) echo "MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET}" >> $GITHUB_ENV ;
+                   echo "XCODEFLAGS=-arch $(uname -m) -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET} MACOSX_DEPLOYMENT_TARGET=${MACOSX_DEPLOYMENT_TARGET}" >> $GITHUB_ENV ;
+                   echo "CFLAGS=${CFLAGS} -mmacosx-version-min=${MACOSX_DEPLOYMENT_TARGET}" >> $GITHUB_ENV ;;
+        esac
+        # staging environment
+        STAGING_DIR='_staging'
+        outputs STAGING_DIR
+        # parse commit reference info
+        echo GITHUB_REF=${GITHUB_REF}
+        echo GITHUB_SHA=${GITHUB_SHA}
+        REF_NAME="${GITHUB_REF#refs/*/}"
+        unset REF_BRANCH ; case "${GITHUB_REF}" in refs/heads/*) REF_BRANCH="${GITHUB_REF#refs/heads/}" ;; esac;
+        unset REF_TAG ; case "${GITHUB_REF}" in refs/tags/*) REF_TAG="${GITHUB_REF#refs/tags/}" ;; esac;
+        REF_SHAS="${GITHUB_SHA:0:8}"
+        outputs REF_BRANCH REF_NAME REF_SHAS REF_TAG
+        # deployable tag? (ie, leading "vM" or "M"; M == version number)
+        unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi
+        outputs DEPLOY
+        # package name
+        PKG_suffix='.tar.gz' ; case '${{ matrix.job.os }}' in windows-*) PKG_suffix='.zip' ;; esac;
+        PKG_VER="${REF_TAG:-git_$REF_SHAS}"
+        PKG_VER="${PKG_VER#v}"
+        PKG_BASENAME="${PROJECT_NAME}-${PKG_VER}${{ matrix.job.fnsuffix }}"
+        PKG_NAME="${PKG_BASENAME}${PKG_suffix}"
+        PKG_DIR="${STAGING_DIR}/${PKG_BASENAME}"
+        outputs PKG_VER PKG_BASENAME PKG_DIR PKG_NAME PKG_suffix
+        COMPRESS_CMD='tar czf'; case '${{ matrix.job.os }}' in windows-*) COMPRESS_CMD='7z -y a' ;; esac;
+        outputs COMPRESS_CMD
+
+    - name: Create/configure any needed build/workspace
+      shell: bash
+      run: |
+        # create build/work space
+        mkdir -p '${{ steps.vars.outputs.STAGING_DIR }}'
+        mkdir -p '${{ steps.vars.outputs.PKG_DIR }}'
+        mkdir -p '${{ steps.vars.outputs.PKG_DIR }}'/bin
+
+    - name: Enable/config MSVC environment (if/when needed)
+      uses: ilammy/msvc-dev-cmd@v1
+      with:
+        arch: "${{ steps.vars.outputs.MSVC_ARCH }}"
+      if: contains(matrix.job.ocaml-version, '+msvc')
+
+    - name: Use OCaml ${{ matrix.job.ocaml-version }}
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: "${{ steps.vars.outputs.OCAML_COMPILER }}"
+        opam-pin: false
+        opam-depext: false
+      # setup-ocaml can prepare the build environment from unison.opam
+      # We're not relying on that capability here, to make sure the builds
+      # also work without using unison.opam
+
+    ## note: at this point, after OCaml installation, windows platforms will use Cygwin bash as the default
+    ##   ... Cygwin bash cannot handle shell scripts containing CRLF EOLs (which are what is generated by GHA on windows platforms)
+    ##   ... so, "igncr" must be added to SHELLOPTS
+
+    - name: Prepare Cygwin environment (Windows)
+      if: runner.os == 'Windows'
+      shell: cmd
+      run: |
+        echo %CYGWIN_ROOT_BIN%>> %GITHUB_PATH%
+        echo %CYGWIN_ROOT_WRAPPERBIN%>> %GITHUB_PATH%
+        echo "/usr/${{ steps.vars.outputs.MinGW_ARCH }}-w64-mingw32/sys-root/mingw/bin">> %GITHUB_PATH%
+        echo SHELLOPTS=igncr>> %GITHUB_ENV%
+
+    - shell: bash
+      run: |
+        opam exec -- make src OSTYPE=$OSTYPE UISTYLE=text STATIC=${{ steps.vars.outputs.STATIC }}
+        # stage
+        # * notes: darwin/macos doesn't build `unison-fsmonitor`
+        for file in ${PROJECT_EXES} ; do
+          if [ -f "src/${file}${{ steps.vars.outputs.EXE_suffix }}" ]; then
+            cp "src/${file}${{ steps.vars.outputs.EXE_suffix }}" '${{ steps.vars.outputs.PKG_DIR }}/bin'
+            echo "'src/${file}${{ steps.vars.outputs.EXE_suffix }}' copied to '${{ steps.vars.outputs.PKG_DIR }}/bin'"
+          fi
+        done
+
+    - run: opam exec -- make test
+
+    ## There is still code to run tests with old ocaml on Windows.
+    ## That remains intentionally so that someone could turn it on if
+    ## desired.
+    - name: Run self-tests over RPC
+      if: runner.os == 'Windows' && !contains(matrix.job.ocaml-version, '4.14')
+      shell: bash
+      run: |
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./src/testbak2 ./src/unison -socket 55443 &
+        sleep 1 # Wait for the server to be fully started
+        ./src/unison -ui text -selftest testr1 socket://127.0.0.1:55443/testr2 -killserver
+
+    - name: Run self-tests over local socket
+      # Recent Windows versions do support Unix domain sockets
+      # but at least OCaml 4.14 is required to use that support
+      if: runner.os != 'Windows' || contains(matrix.job.ocaml-version, '4.14')
+      shell: bash
+      run: |
+        mkdir localsocket
+        chmod 700 localsocket
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./src/testbak4 ./src/unison -socket ./localsocket/test.sock &
+        sleep 1 # Wait for the server to be fully started
+        ${{ runner.os == 'Windows' }} || test -S ./localsocket/test.sock
+        ./src/unison -ui text -selftest testr3 socket://{./localsocket/test.sock}/testr4 -killserver
+
+    - name: Prepare lablgtk install (Windows)
+      if: ${{ runner.os == 'Windows' && contains(matrix.job.ocaml-version, '+mingw') }}
+      shell: bash
+      run: |
+        opam install opam-depext depext-cygwinports
+        setup-x86_64.exe --quiet-mode --root "${CYGWIN_ROOT}" --site http://cygwin.mirror.constant.com --symlink-type=sys --packages hicolor-icon-theme,adwaita-icon-theme
+        # [2022-11] This terrible (terrible) hack is here to forcibly skip
+        # building the fontconfig cache because it can take 30-45 minutes
+        # on GHA runners and is never needed anyway.
+        setup-x86_64.exe --quiet-mode --root "${CYGWIN_ROOT}" --site http://cygwin.mirror.constant.com --symlink-type=sys --local-package-dir D:/a --download --packages mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig
+        cd 'D:/a/https%3a%2f%2fcygwin.mirror.constant.com%2f/noarch/release/'mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig
+        CNAMEXZ=$(ls mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig*.tar.xz)
+        CNAME=${CNAMEXZ%.xz}
+        unxz ${CNAMEXZ}
+        tar --delete --file ${CNAME} etc/postinstall/zp_mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig_cache.sh
+        xz ${CNAME}
+        sha512sum > sha512.sum
+        CSZ=$(stat -c %s ${CNAMEXZ})
+        SHASUM=$(sha512sum ${CNAMEXZ})
+        cd 'D:/a/https%3a%2f%2fcygwin.mirror.constant.com%2f/x86_64'
+        mv setup.ini tsetup.ini
+        rm -f setup*
+        sed -E -e "\|install: noarch/release/mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig/${CNAMEXZ}|s/xz .+/xz ${CSZ} ${SHASUM%% *}/" tsetup.ini > setup.ini
+        rm tsetup.ini
+        sha512sum > sha512.sum
+        setup-x86_64.exe --quiet-mode --root "${CYGWIN_ROOT}" --symlink-type=sys --local-install --local-package-dir 'D:/a/https%3a%2f%2fcygwin.mirror.constant.com%2f' --mirror-mode --no-verify --packages mingw64-${{ steps.vars.outputs.MinGW_ARCH }}-fontconfig
+
+    - name: lablgtk install
+      ## [2020-09] non-working/unavailable for MSVC or musl OCaml variants ; also, non-working for 32bit OCaml variant (see [GH:garrigue/lablgtk#64](https://github.com/garrigue/lablgtk/issues/64))
+      if: ${{ ! ( contains(matrix.job.ocaml-version, '+msvc') || contains(matrix.job.ocaml-version, '-musl') || contains(matrix.job.ocaml-version, '-32bit') ) }}
+      run: opam depext --install --verbose --yes lablgtk3 && opam install ocamlfind
+
+    - if: steps.vars.outputs.STATIC != 'true' ## unable to build static gtk/gui
+      shell: bash
+      run: |
+        opam exec -- make src OSTYPE=$OSTYPE UISTYLE=gtk3 STATIC=${{ steps.vars.outputs.STATIC }}
+        # stage
+        # * copy only main/first project binary
+        project_exe_stem=${PROJECT_EXES%% *}
+        cp "src/${project_exe_stem}${{ steps.vars.outputs.EXE_suffix }}" "${{ steps.vars.outputs.PKG_DIR }}/bin/${project_exe_stem}-gui${{ steps.vars.outputs.EXE_suffix }}"
+
+    - name: "Build WinOS text+gui hybrid"
+      if: runner.os == 'Windows' && steps.vars.outputs.STATIC != 'true' ## WinOS, non-static (unable to build static gtk/gui)
+      shell: bash
+      run: |
+        # create and stage text+gui hybrid for Windows
+        # * copy only main/first project binary
+        project_exe_stem=${PROJECT_EXES%% *}
+        # * clean/remove build artifact(s)
+        rm "src/${project_exe_stem}${{ steps.vars.outputs.EXE_suffix }}" ##.or.# opam exec -- make -C src clean #.or.# opam exec -- make clean
+        # * re-create (with hybrid text+gui UI)
+        opam exec -- make src OSTYPE=$OSTYPE UISTYLE=gtk3 UI_WINOS=hybrid STATIC=${{ steps.vars.outputs.STATIC }}
+        cp "src/${project_exe_stem}${{ steps.vars.outputs.EXE_suffix }}" "${{ steps.vars.outputs.PKG_DIR }}/bin/${project_exe_stem}-text+gui${{ steps.vars.outputs.EXE_suffix }}"
+
+    - uses: actions/upload-artifact@v3
+      with:
+        name: unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}
+        path: ${{ steps.vars.outputs.PKG_DIR }}/bin/*
+
+    - name: Copy user manual
+      continue-on-error: ${{ !(steps.vars.outputs.DEPLOY && matrix.job.publish) }}
+      uses: actions/download-artifact@v3
+      with:
+        name: unison-docs
+        path: '${{ steps.vars.outputs.PKG_DIR }}'
+
+    - name: Prepare package
+      # if: steps.vars.outputs.DEPLOY
+      shell: bash
+      run: |
+        ## package artifact(s)
+        PKG_DIR='${{ steps.vars.outputs.PKG_DIR }}'
+        # `strip` binaries
+        strip "${PKG_DIR}/bin"/*'${{ steps.vars.outputs.EXE_suffix }}'
+        # README and LICENSE
+        (shopt -s nullglob; for f in [R]'EADME'{,.*}; do cp $f "${PKG_DIR}"/ ; done)
+        (shopt -s nullglob; for f in [L]'ICENSE'{-*,}{,.*}; do cp $f "${PKG_DIR}"/ ; done)
+
+    - if: runner.os == 'Windows'
+      name: "Windows: Package gtk"
+      shell: bash
+      run: |
+        # [2023-03] Setting this PATH here (and it has to be right here) is
+        # a workaround for an unknown issue (most likely something with GHA)
+        # causing MinGW binutils not to be found (while other binutils
+        # pre-installed in the GHA images may be found instead), which in turn
+        # causes the DLL extracting functions to silently fail.
+        export PATH="/usr/${{ steps.vars.outputs.MinGW_ARCH }}-w64-mingw32/bin":${PATH}
+        ## package artifact(s)
+        PKG_DIR='${{ steps.vars.outputs.PKG_DIR }}'
+        # collect any needed dlls/libraries
+        # dlls
+        dll_refs() { eval "$(opam env)" ; eval "$(ocaml-env cygwin)" ; objdump -x "$@" | grep -Po "\S+[.]dll$" | xargs -I{} 2>/dev/null which "{}" | sort -u ; }
+        filtered_dll_refs() { list="$(dll_refs "$@" | grep -vF "$(cygpath ${WINDIR})" | perl -lape '$_ = qq/@{[sort @F]}/')" ; echo "$list" ; }
+        recursive_filtered_dll_refs() { list="$(filtered_dll_refs "$@")" ; n=0 ; while [ $n -lt $(echo "$list" | wc -l) ]; do n=$(echo "$list" | wc -l) ; list="$(filtered_dll_refs $list)" ; done ; echo "$list" ; }
+        IFS=$'\n' DLL_list=( "$(recursive_filtered_dll_refs "${PKG_DIR}"/bin/*)" )
+        for dll in ${DLL_list[@]} ; do cp "${dll}" "${PKG_DIR}"/bin ; done
+        TARGET_ARCH_ID='x86_64'; case '${{ matrix.job.ocaml-version }}' in *+mingw32*|*+msvc32*) TARGET_ARCH_ID='i686' ;; esac
+        # required gdk support files
+        mkdir "${PKG_DIR}"/lib
+        cp -r /usr/${TARGET_ARCH_ID}-w64-mingw32/sys-root/mingw/lib/gdk-pixbuf-2.0 "${PKG_DIR}"/lib/
+        # update loader.cache to point to local relative installation
+        mv "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original
+        cat "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original | sed -E 's#([^"]*)(lib/gdk-pixbuf-2.0/2.10.0/loaders/[^"]*[.]dll)#../\2#' > "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
+        rm "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original
+        # required icons
+        mkdir "${PKG_DIR}"/share
+        cp -rL /usr/share/icons "${PKG_DIR}"/share
+        # compile glib settings schema
+        mkdir -p "${PKG_DIR}"/share/glib-2.0
+        cp -r /usr/${TARGET_ARCH_ID}-w64-mingw32/sys-root/mingw/share/glib-2.0/schemas "${PKG_DIR}"/share/glib-2.0
+        glib-compile-schemas "${PKG_DIR}"/share/glib-2.0/schemas
+        # add gtk configuration
+        mkdir -p "${PKG_DIR}"/etc/gtk-3.0
+        printf "[Settings]\ngtk-button-images=true\ngtk-font-name=Segoe UI 9\n" > "${PKG_DIR}"/etc/gtk-3.0/settings.ini
+
+    - name: Package
+      shell: bash
+      run: cd '${{ steps.vars.outputs.PKG_DIR }}/' && ${{ steps.vars.outputs.COMPRESS_CMD }} '../${{ steps.vars.outputs.PKG_NAME }}' *
+
+    - uses: actions/upload-artifact@v3
+      if: matrix.job.publish
+      with:
+        name: unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}-publish
+        path: ${{ steps.vars.outputs.STAGING_DIR }}/${{ steps.vars.outputs.PKG_NAME }}
+
+    - name: Publish
+      if: steps.vars.outputs.DEPLOY && matrix.job.publish
+      uses: softprops/action-gh-release@v1
+      with:
+        files: |
+          ${{ steps.vars.outputs.STAGING_DIR }}/${{ steps.vars.outputs.PKG_NAME }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+    - if: runner.os == 'macOS'
+      name: "macOS: Build and package Unison.app"
+      id: macapp
+      run: |
+        opam exec -- make src UISTYLE=mac
+
+        # package
+        APP_NAME=Unison-${{ steps.vars.outputs.PKG_VER }}-macos.app.tar.gz
+        echo APP_NAME=${APP_NAME} >> $GITHUB_OUTPUT
+
+        tar czf ${APP_NAME} -C src/uimac/build/Default Unison.app
+
+    - if: runner.os == 'macOS'
+      name: "macOS: Upload Unison.app artifact"
+      uses: actions/upload-artifact@v3
+      with:
+        name: Unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}.app
+        path: ${{ steps.macapp.outputs.APP_NAME }}
+
+    - if: runner.os == 'macOS' && steps.vars.outputs.DEPLOY && matrix.job.publish
+      name: "macOS: Publish Unison.app"
+      uses: softprops/action-gh-release@v1
+      with:
+        files: ${{ steps.macapp.outputs.APP_NAME }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+
+  rpc_abicheck:
+    needs: build
+
+    strategy:
+      fail-fast: false
+      matrix:
+        # This list is intended to balance good enough coverage and
+        # limited resource usage.
+        job:
+        - { os: ubuntu-22.04  , ocaml-version: 5.0.x   , ref: v2.53.0 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.14.x  , ref: v2.53.0 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.14.x  , ref: v2.52.1 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.14.x  , ref: v2.51.5 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.08.x  , ref: v2.51.5 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.08.x  , ref: v2.51.2 }
+        - { os: ubuntu-22.04  , ocaml-version: 4.08.x  , ref: 2.48.4 }
+        - { os: windows-2019  , ocaml-version: ocaml-variants.4.14.0+mingw64c  , ref: v2.53.0 }
+        - { os: windows-2019  , ocaml-version: ocaml-variants.4.14.0+mingw64c  , ref: v2.52.1 }
+        - { os: windows-2019  , ocaml-version: ocaml-variants.4.08.1+mingw32c  , ref: v2.51.5 }
+        - { os: windows-2019  , ocaml-version: ocaml-variants.4.08.1+mingw64c  , ref: v2.51.2 }
+        - { os: windows-2019  , ocaml-version: ocaml-variants.4.08.1+mingw64c  , ref: 2.48.4 }
+        - { os: macos-12      , ocaml-version: 4.14.x  , ref: v2.53.0 }
+        - { os: macos-12      , ocaml-version: 4.14.x  , ref: v2.52.1 }
+        - { os: macos-12      , ocaml-version: 4.08.x  , ref: v2.51.5 }
+        - { os: macos-12      , ocaml-version: 4.08.x  , ref: v2.51.2 }
+        - { os: macos-12      , ocaml-version: 4.08.x  , ref: 2.48.4 }
+
+    runs-on: ${{ matrix.job.os }}
+
+    steps:
+    - if: contains(matrix.job.os, 'ubuntu')
+      run: sudo apt-get update
+
+    - name: Use OCaml ${{ matrix.job.ocaml-version }}
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: "${{ matrix.job.ocaml-version }}"
+        opam-pin: false
+        opam-depext: false
+      # setup-ocaml can prepare the build environment from unison.opam
+      # We're not relying on that capability here, to make sure the builds
+      # also work without using unison.opam
+
+    - name: Prepare Cygwin environment (Windows)
+      if: runner.os == 'Windows'
+      shell: cmd
+      run: |
+        echo %CYGWIN_ROOT_BIN%>> %GITHUB_PATH%
+        echo %CYGWIN_ROOT_WRAPPERBIN%>> %GITHUB_PATH%
+        echo SHELLOPTS=igncr>> %GITHUB_ENV%
+
+    - name: Checkout HEAD to _new
+      uses: actions/checkout@v3
+      with:
+        path: _new
+
+    - name: "2.48: Patch tests in _new"
+      if: contains(matrix.job.ref, '2.48')
+      shell: bash
+      run: |
+        # 'atomic' was introduced in 2.51.0
+        cd _new && git apply - <<"EOF"
+        diff --git a/src/test.ml b/src/test.ml
+        index 60ccd05..45395e5 100644
+        --- a/src/test.ml
+        +++ b/src/test.ml
+        @@ -351,43 +351,6 @@ let test() =
+             )
+           done;
+         
+        -  (* Test that .git is treated atomically. *)
+        -  runtest "Atomicity of certain directories 1" ["atomic = Name .git";
+        -                                                "force = newer"] (fun() ->
+        -      let orig = (Dir ["foo", Dir [".git", Dir ["a", File "foo";
+        -                                                "b", File "bar";
+        -                                                "c", File "baz"]]]) in
+        -      put R1 orig;
+        -      Unix.sleep 2; (* in case time granularity is coarse on this FS *)
+        -      put R2 orig; sync();
+        -      let expected = (Dir ["foo", Dir [".git", Dir ["a", File "modified on R1";
+        -                                                    "b", File "bar";
+        -                                                    "c", File "modified on R1"]]]) in
+        -      put R2 (Dir ["foo", Dir [".git",
+        -                               Dir ["a", File "foo";
+        -                                    "b", File "modified on R2";
+        -                                    "c", File "modified on R2"]]]);
+        -      Unix.sleep 2; 
+        -      put R1 expected;
+        -      sync ();
+        -      check "1" R2 expected;
+        -      check "2" R1 expected
+        -    );
+        -
+        -  runtest "Atomicity of certain directories 2" ["atomic = Name .git"] (fun() ->
+        -      let a = (Dir ["foo", Dir [".git", Dir ["a", File "foo";
+        -                                             "b", File "bar";
+        -                                             "c", File "baz";
+        -                                             "d", File "quux"]]]) in
+        -      let b = (Dir ["foo", Dir [".git", Dir ["a", File "foo";
+        -                                             "b", File "bar";
+        -                                             "c", File "baz";
+        -                                             "e", File "quux"]]]) in
+        -      put R1 a; put R2 b; sync();
+        -      check "1" R1 a;
+        -      check "2" R2 b
+        -    );
+        -
+           (* Check for the bug reported by Ralf Lehmann *)
+           if not bothRootsLocal then
+             runtest "backups 1 (remote)" ["backup = Name *"] (fun() ->
+        EOF
+
+    - run: cd _new && opam exec -- make src UISTYLE=text OSTYPE=$OSTYPE
+      shell: bash
+
+    - name: Checkout ${{ matrix.job.ref }} to _prev
+      uses: actions/checkout@v3
+      with:
+        ref: "${{ matrix.job.ref }}"
+        path: _prev
+
+    - name: "2.48: Patch tests in _prev"
+      if: contains(matrix.job.ref, '2.48')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/test.ml b/src/test.ml
+        index 6e8f943c..6c0bbded 100644
+        --- a/src/test.ml
+        +++ b/src/test.ml
+        @@ -324,7 +324,7 @@ let test() =
+        
+           (* Check for the bug reported by Ralf Lehmann *)
+           if not bothRootsLocal then 
+        -    runtest "backups 1 (remote)" ["backup = Name *"] (fun() -> 
+        +    runtest "backups 1 (remote)" ["backup = Name *"; "fastcheck = false"] (fun() ->
+               put R1 (Dir []); put R2 (Dir []); sync();
+               debug (fun () -> Util.msg "First check\n");
+               checkmissing "1" BACKUP1;
+        @@ -370,7 +370,6 @@ let test() =
+               check "4" R2 (Dir ["x", File "foo"]);
+             );
+        
+        -  (raise (Util.Fatal "Skipping some tests -- remove me!\n") : unit); 
+        
+           if bothRootsLocal then 
+             runtest "backups 1 (local)" ["backup = Name *"] (fun() -> 
+        @@ -397,12 +397,12 @@ let test() =
+             check "1" R2 (Dir [(".bak.0.x", File "foo"); (".bak.0.d", Dir [("a", File "barr")])]);
+           );
+        
+        -  runtest "backups 2a" ["backup = Name *"; "backuplocation = local"] (fun() -> 
+        +  runtest "backups 2a" ["backup = Name *"; "backuplocation = local"; "fastcheck = false"] (fun() ->
+             put R1 (Dir []); put R2 (Dir []); sync();
+             (* Create a file and a directory *)
+             put R1 (Dir ["foo", File "1"]); sync();
+             check "1" R1 (Dir [("foo", File "1")]);
+        -    check "2" R1 (Dir [("foo", File "1")]);
+        +    check "2" R2 (Dir [("foo", File "1")]);
+             put R1 (Dir ["foo", File "2"]); sync();
+             check "3" R1 (Dir [("foo", File "2")]);
+             check "4" R2 (Dir [("foo", File "2"); (".bak.0.foo", File "1")]); 
+        @@ -487,6 +486,77 @@ let test() =
+             );
+           end;
+         
+        +  if not bothRootsLocal then
+        +  begin
+        +    let localR, remoteR, localRaw =
+        +      match r1 with
+        +      | Common.Local, _ -> R1, R2, r1
+        +      | _ -> R2, R1, r2
+        +    in
+        +
+        +    (* Test RPC function "fingerprintSubfile" *)
+        +    runtest "RPC: transfer append" [] (fun () ->
+        +      let prefixLen = 1024 * 1024 + 1 in
+        +      let len = prefixLen + 31 in
+        +      let contents = String.make len '.' in
+        +      let fileName = "bigfile" in
+        +      let prefixPath = Path.fromString fileName in
+        +      let (workingDir, _) = Fspath.findWorkingDir (snd localRaw) prefixPath in
+        +      let prefixName = Path.toString (Os.tempPath ~fresh:false workingDir prefixPath) in
+        +      put remoteR (Dir [(fileName, File contents)]);
+        +      put localR (Dir [(prefixName, File (String.sub contents 0 prefixLen))]);
+        +      sync ();
+        +      check "1" localR (Dir [(fileName, File contents)]);
+        +    );
+        +
+        +    (* Test RPC function "updateProps" *)
+        +    runtest "RPC: update props" ["times = true"] (fun () ->
+        +      let state = [("a", File "x")] in
+        +      put remoteR (Dir state);
+        +      put localR (Dir []);
+        +      sync ();
+        +      (* Having to sleep here is an unfortunate side-effect of the current
+        +         Windows limitations-inspired time comparison algorithm which is
+        +         designed to work on FAT filesystems (2-second granularity). *)
+        +      Unix.sleep 2;
+        +      put remoteR (Dir state);
+        +      sync ();
+        +      check "1" localR (Dir state);
+        +    );
+        +
+        +    (* Test RPC function "replaceArchive" *)
+        +    runtest "RPC: replaceArchive" [] (fun () ->
+        +      put localR (Dir [("n", File "to delete")]);
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      check "1" localR (Dir []);
+        +    );
+        +
+        +    (* Test RPC functions "mkdir" and "setDirProp" *)
+        +    runtest "RPC: mkdir, setDirProp" [] (fun () ->
+        +      let state = [("subd", Dir [])] in
+        +      put localR (Dir state);
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      check "1" remoteR (Dir state);
+        +    );
+        +
+        +    (* Test RPC function "setupTargetPaths" *)
+        +    runtest "RPC: merge" ["merge = Name ma -> echo x> NEW"; "backupcurr = Name ma"; "fastcheck = false"] (fun () ->
+        +      let result = match Sys.os_type with
+        +        | "Win32" -> ("ma", File "x\r\n")
+        +        | _ -> ("ma", File "x\n")
+        +      in
+        +      put localR (Dir [("ma", File "a")]);
+        +      put remoteR (Dir [("ma", File "b")]);
+        +      sync ();
+        +      check "1" localR (Dir [result]);
+        +      check "2" remoteR (Dir [result]);
+        +    );
+        +  end;
+        +
+           if !failures = 0 then
+             Util.msg "Success :-)\n"
+           else
+        EOF
+
+    - name: "2.48: Patch bugs in _prev"
+      if: contains(matrix.job.ref, '2.48')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/bytearray_stubs.c b/src/bytearray_stubs.c
+        index 1ec18aef..664e3d96 100644
+        --- a/src/bytearray_stubs.c
+        +++ b/src/bytearray_stubs.c
+        @@ -9,7 +9,7 @@
+         CAMLprim value ml_marshal_to_bigarray(value v, value flags)
+         {
+           char *buf;
+        -  long len;
+        +  intnat len;
+           output_value_to_malloc(v, flags, &buf, &len);
+           return alloc_bigarray(BIGARRAY_UINT8 | BIGARRAY_C_LAYOUT | BIGARRAY_MANAGED,
+                                 1, buf, &len);
+        EOF
+
+    - name: "2.48: Patch _prev for mingw compilers"
+      if: contains(matrix.job.ref, '2.48')
+      shell: bash
+      run: |
+        cd _prev && git apply --ignore-whitespace - <<"EOF"
+        diff --git a/src/Makefile.OCaml b/src/Makefile.OCaml
+        index 21610ce6..4d605bbd 100644
+        --- a/src/Makefile.OCaml
+        +++ b/src/Makefile.OCaml
+        @@ -104,8 +104,8 @@ CAMLFLAGS+=-I system/$(SYSTEM) -I lwt/$(SYSTEM)
+         ifeq ($(OSARCH),win32)
+           # Win32 system
+           EXEC_EXT=.exe
+        -  OBJ_EXT=.obj
+        -  OUTPUT_SEL=/Fo
+        +  OBJ_EXT=.o
+        +  OUTPUT_SEL=-o
+           CWD=.
+         #  Fix suggested by Karl M, Jan 2009:
+         #    "The new flexlink wrapper that OCaml 3.11 uses was gagging on the res
+        @@ -117,8 +117,6 @@ ifeq ($(OSARCH),win32)
+           COBJS+=system/system_win_stubs$(OBJ_EXT) lwt/lwt_unix_stubs$(OBJ_EXT)
+           WINOBJS=system/system_win.cmo
+           SYSTEM=win
+        -  CLIBS+=-cclib "-link win32rc/unison.res" shell32.lib
+        -  STATICLIBS+=-cclib "-link win32rc/unison.res" shell32.lib
+           buildexecutable::
+         	@echo Building for Windows
+         else
+        diff --git a/src/lwt/lwt_unix_stubs.c b/src/lwt/lwt_unix_stubs.c
+        index aa85e5bb..3717ddc2 100644
+        --- a/src/lwt/lwt_unix_stubs.c
+        +++ b/src/lwt/lwt_unix_stubs.c
+        @@ -79,6 +79,7 @@ static value completionCallback;
+        
+         static void invoke_completion_callback
+         (long id, long len, long errCode, long action) {
+        +  CAMLparam0();
+           CAMLlocal2 (err, name);
+           value args[4];
+           err = Val_long(0);
+        EOF
+
+    - name: "2.48: Patch _prev for newer compilers/OS"
+      if: contains(matrix.job.ref, '2.48') && matrix.job.ocaml-version >= '4.03'
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/Makefile.OCaml b/src/Makefile.OCaml
+        index 21610ce6..75499079 100644
+        --- a/src/Makefile.OCaml
+        +++ b/src/Makefile.OCaml
+        @@ -95,7 +95,7 @@ buildexecutable::
+         ### Default parameters
+        
+         # Generate backtrace information for exceptions
+        -CAMLFLAGS+=-g
+        +CAMLFLAGS+=-g -unsafe-string
+        
+         INCLFLAGS=-I lwt -I ubase -I system
+         CAMLFLAGS+=$(INCLFLAGS)
+        @@ -187,7 +187,7 @@ endif
+         endif
+         endif
+         
+        -MINOSXVERSION=10.5
+        +MINOSXVERSION=10.7
+         # XCODEFLAGS=-sdk macosx$(MINOSXVERSION)
+         ifeq ($(OSARCH),osx)
+           CAMLFLAGS+=-ccopt -mmacosx-version-min=$(MINOSXVERSION)
+        diff --git a/src/files.ml b/src/files.ml
+        index ba42ad57..5babf21e 100644
+        --- a/src/files.ml
+        +++ b/src/files.ml
+        @@ -722,7 +722,7 @@ let get_files_in_directory dir =
+           with End_of_file ->
+             dirh.System.closedir ()
+           end;
+        -  Sort.list (<) !files
+        +  List.sort String.compare !files
+        
+         let ls dir pattern =
+           Util.convertUnixErrorsToTransient
+        diff --git a/src/recon.ml b/src/recon.ml
+        index 5ed358d7..0df2cfe4 100644
+        --- a/src/recon.ml
+        +++ b/src/recon.ml
+        @@ -651,8 +651,8 @@ let rec reconcile
+        
+         (* Sorts the paths so that they will be displayed in order                   *)
+         let sortPaths pathUpdatesList =
+        -  Sort.list
+        -    (fun (p1, _) (p2, _) -> Path.compare p1 p2 <= 0)
+        +  List.sort
+        +    Path.compare
+             pathUpdatesList
+        
+         let rec enterPath p1 p2 t =
+        diff --git a/src/system/system_generic.ml b/src/system/system_generic.ml
+        index 9230cdc1..140bd849 100755
+        --- a/src/system/system_generic.ml
+        +++ b/src/system/system_generic.ml
+        @@ -47,7 +47,7 @@ let open_out_gen = open_out_gen
+         let chmod = Unix.chmod
+         let chown = Unix.chown
+         let utimes = Unix.utimes
+        -let link = Unix.link
+        +let link x y = Unix.link x y
+         let openfile = Unix.openfile
+         let opendir f =
+           let h = Unix.opendir f in
+        EOF
+
+    - name: "2.51.0: Patch connection header in _prev"
+      if: contains(matrix.job.ref, '2.51.0')
+      shell: bash
+      run: |
+        # Connection header string was broken in 2.51.0
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/remote.ml b/src/remote.ml
+        index ddca0d77..7f819eab 100644
+        --- a/src/remote.ml
+        +++ b/src/remote.ml
+        @@ -920,7 +920,7 @@ let connectionHeader =
+             Scanf.sscanf Sys.ocaml_version "%d.%d.%d" (fun x y z -> (x,y,z)) in
+           let compiler =
+             if    major < 4 
+        -       || major = 4 && minor <= 2
+        +       || major = 4 && minor < 2
+                || major = 4 && minor = 2 && patchlevel <= 1
+             then "<= 4.01.1"
+             else ">= 4.01.2"
+        EOF
+
+    - name: "2.51.{2,3}: Patch bugs in _prev"
+      if: contains(matrix.job.ref, '2.51.2') || contains(matrix.job.ref, '2.51.3')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/bytearray_stubs.c b/src/bytearray_stubs.c
+        index 2b29421a..2850f2d8 100644
+        --- a/src/bytearray_stubs.c
+        +++ b/src/bytearray_stubs.c
+        @@ -10,7 +10,7 @@
+         CAMLprim value ml_marshal_to_bigarray(value v, value flags)
+         {
+           char *buf;
+        -  long len;
+        +  intnat len;
+           output_value_to_malloc(v, flags, &buf, &len);
+           return alloc_bigarray(BIGARRAY_UINT8 | BIGARRAY_C_LAYOUT | BIGARRAY_MANAGED,
+                                 1, buf, &len);
+        diff --git a/src/uicommon.ml b/src/uicommon.ml
+        index 9fa94cf5..65fc37a5 100644
+        --- a/src/uicommon.ml
+        +++ b/src/uicommon.ml
+        @@ -494,10 +494,11 @@ let promptForRoots getFirstRoot getSecondRoot =
+         (* ---- *)
+         
+         let makeTempDir pattern =
+        -  let ic = Unix.open_process_in (Printf.sprintf "(mktemp --tmpdir -d %s.XXXXXX || mktemp -d -t %s) 2>/dev/null" pattern pattern) in
+        -  let path = input_line ic in
+        -  ignore (Unix.close_process_in ic);
+        -  path
+        +  let path = Filename.temp_file pattern "" in
+        +  let fspath = System.fspathFromString path in
+        +  System.unlink fspath; (* Remove file created by [temp_file]... *)
+        +  System.mkdir fspath 0o755; (* ... and create a dir instead. *)
+        +  path ^ Filename.dir_sep
+         
+         (* The first time we load preferences, we also read the command line
+            arguments; if we re-load prefs (because the user selected a new profile)
+        EOF
+
+    - name: "2.51.2: Patch _prev for mingw compilers"
+      if: contains(matrix.job.ref, '2.51.2')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/Makefile.OCaml b/src/Makefile.OCaml
+        index 7cefa2ec..95b1bec4 100644
+        --- a/src/Makefile.OCaml
+        +++ b/src/Makefile.OCaml
+        @@ -107,8 +107,8 @@ CAMLFLAGS+=-I system/$(SYSTEM) -I lwt/$(SYSTEM)
+         ifeq ($(OSARCH),win32)
+           # Win32 system
+           EXEC_EXT=.exe
+        -  OBJ_EXT=.obj
+        -  OUTPUT_SEL=/Fo
+        +  OBJ_EXT=.o
+        +  OUTPUT_SEL=-o
+           CWD=.
+         #  Fix suggested by Karl M, Jan 2009:
+         #    "The new flexlink wrapper that OCaml 3.11 uses was gagging on the res
+        @@ -120,8 +120,6 @@ ifeq ($(OSARCH),win32)
+           COBJS+=system/system_win_stubs$(OBJ_EXT) lwt/lwt_unix_stubs$(OBJ_EXT)
+           WINOBJS=system/system_win.cmo
+           SYSTEM=win
+        -  CLIBS+=-cclib "-link win32rc/unison.res" shell32.lib
+        -  STATICLIBS+=-cclib "-link win32rc/unison.res" shell32.lib
+           buildexecutable::
+         	@echo Building for Windows
+         else
+        EOF
+
+    - name: "2.51.2: Patch _prev for newer compilers/OS"
+      if: contains(matrix.job.ref, '2.51.2')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/Makefile.OCaml b/src/Makefile.OCaml
+        index 7cefa2ec..ffc3ab9e 100644
+        --- a/src/Makefile.OCaml
+        +++ b/src/Makefile.OCaml
+        @@ -180,7 +180,7 @@ else
+           buildexecutable:: $(NAME)$(EXEC_EXT)
+         endif
+         
+        -MINOSXVERSION=10.5
+        +MINOSXVERSION=10.7
+         # XCODEFLAGS=-sdk macosx$(MINOSXVERSION)
+         ifeq ($(OSARCH),osx)
+           CAMLFLAGS+=-ccopt -mmacosx-version-min=$(MINOSXVERSION)
+        diff --git a/src/files.ml b/src/files.ml
+        index 5ff18810..1d1fbcc6 100644
+        --- a/src/files.ml
+        +++ b/src/files.ml
+        @@ -734,7 +734,7 @@ let get_files_in_directory dir =
+           with End_of_file ->
+             dirh.System.closedir ()
+           end;
+        -  Sort.list (<) !files
+        +  List.sort String.compare !files
+        
+         let ls dir pattern =
+           Util.convertUnixErrorsToTransient
+        diff --git a/src/recon.ml b/src/recon.ml
+        index 2c619bb8..2412c18e 100644
+        --- a/src/recon.ml
+        +++ b/src/recon.ml
+        @@ -661,8 +661,8 @@ let rec reconcile
+        
+         (* Sorts the paths so that they will be displayed in order                   *)
+         let sortPaths pathUpdatesList =
+        -  Sort.list
+        -    (fun (p1, _) (p2, _) -> Path.compare p1 p2 <= 0)
+        +  List.sort
+        +    Path.compare
+             pathUpdatesList
+        
+         let rec enterPath p1 p2 t =
+        diff --git a/src/system/system_generic.ml b/src/system/system_generic.ml
+        index 453027d0..290851e1 100755
+        --- a/src/system/system_generic.ml
+        +++ b/src/system/system_generic.ml
+        @@ -47,7 +47,7 @@ let open_out_gen = open_out_gen
+         let chmod = Unix.chmod
+         let chown = Unix.chown
+         let utimes = Unix.utimes
+        -let link = Unix.link
+        +let link x y = Unix.link x y
+         let openfile = Unix.openfile
+         let opendir f =
+           let h = Unix.opendir f in
+        EOF
+
+    - name: "2.51.0 - 2.52.1: Patch tests in _prev"
+      if: contains(matrix.job.ref, '2.51') || matrix.job.ref == 'v2.52.0' || matrix.job.ref == 'v2.52.1'
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/test.ml b/src/test.ml
+        index 3d480409..60ed014d 100644
+        --- a/src/test.ml
+        +++ b/src/test.ml
+        @@ -542,6 +542,77 @@ let test() =
+             *)
+           end;
+         
+        +  if not bothRootsLocal then
+        +  begin
+        +    let localR, remoteR, localRaw =
+        +      match r1 with
+        +      | Common.Local, _ -> R1, R2, r1
+        +      | _ -> R2, R1, r2
+        +    in
+        +
+        +    (* Test RPC function "fingerprintSubfile" *)
+        +    runtest "RPC: transfer append" [] (fun () ->
+        +      let prefixLen = 1024 * 1024 + 1 in
+        +      let len = prefixLen + 31 in
+        +      let contents = String.make len '.' in
+        +      let fileName = "bigfile" in
+        +      let prefixPath = Path.fromString fileName in
+        +      let (workingDir, _) = Fspath.findWorkingDir (snd localRaw) prefixPath in
+        +      let prefixName = Path.toString (Os.tempPath ~fresh:false workingDir prefixPath) in
+        +      put remoteR (Dir [(fileName, File contents)]);
+        +      put localR (Dir [(prefixName, File (String.sub contents 0 prefixLen))]);
+        +      sync ();
+        +      check "1" localR (Dir [(fileName, File contents)]);
+        +    );
+        +
+        +    (* Test RPC function "updateProps" *)
+        +    runtest "RPC: update props" ["times = true"] (fun () ->
+        +      let state = [("a", File "x")] in
+        +      put remoteR (Dir state);
+        +      put localR (Dir []);
+        +      sync ();
+        +      (* Having to sleep here is an unfortunate side-effect of the current
+        +         Windows limitations-inspired time comparison algorithm which is
+        +         designed to work on FAT filesystems (2-second granularity). *)
+        +      Unix.sleep 2;
+        +      put remoteR (Dir state);
+        +      sync ();
+        +      check "1" localR (Dir state);
+        +    );
+        +
+        +    (* Test RPC function "replaceArchive" *)
+        +    runtest "RPC: replaceArchive" [] (fun () ->
+        +      put localR (Dir [("n", File "to delete")]);
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      check "1" localR (Dir []);
+        +    );
+        +
+        +    (* Test RPC functions "mkdir" and "setDirProp" *)
+        +    runtest "RPC: mkdir, setDirProp" [] (fun () ->
+        +      let state = [("subd", Dir [])] in
+        +      put localR (Dir state);
+        +      put remoteR (Dir []);
+        +      sync ();
+        +      check "1" remoteR (Dir state);
+        +    );
+        +
+        +    (* Test RPC function "setupTargetPaths" *)
+        +    runtest "RPC: merge" ["merge = Name ma -> echo x> NEW"; "backupcurr = Name ma"] (fun () ->
+        +      let result = match Sys.os_type with
+        +        | "Win32" -> ("ma", File "x\r\n")
+        +        | _ -> ("ma", File "x\n")
+        +      in
+        +      put localR (Dir [("ma", File "a")]);
+        +      put remoteR (Dir [("ma", File "b")]);
+        +      sync ();
+        +      check "1" localR (Dir [result]);
+        +      check "2" remoteR (Dir [result]);
+        +    );
+        +  end;
+        +
+           if !failures = 0 then
+             Util.msg "Success :-)\n"
+           else
+        EOF
+
+    - name: "2.52.0: Patch _prev for newer compilers"
+      if: contains(matrix.job.ref, '2.52.0')
+      shell: bash
+      run: |
+        cd _prev && git apply - <<"EOF"
+        diff --git a/src/system/system_win_stubs.c b/src/system/system_win_stubs.c
+        index 50ea663f..57940d98 100644
+        --- a/src/system/system_win_stubs.c
+        +++ b/src/system/system_win_stubs.c
+        @@ -373,7 +373,7 @@ typedef enum _FILE_INFORMATION_CLASS {
+         #include <caml/version.h> /* Available since OCaml 4.02 */
+         #endif
+        
+        -#if !defined(OCAML_VERSION) || OCAML_VERSION < 40300
+        +#if !defined(OCAML_VERSION) || OCAML_VERSION < 40300 || OCAML_VERSION >= 41400
+        
+         typedef struct _REPARSE_DATA_BUFFER {
+           ULONG  ReparseTag;
+        EOF
+
+    - run: cd _prev && opam exec -- make src UISTYLE=text OSTYPE=$OSTYPE
+      shell: bash
+
+    # IMPORTANT! These tests do not exercise the entire RPC API. Yet, they
+    # should work fine as a smoke test.
+
+    - name: Run self-tests over RPC - new client to prev server
+      env:
+        UNISON: test1
+      shell: bash
+      run: |
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./_prev/src/testbak_s _prev/src/unison -socket 55443 &
+        sleep 1 # Wait for the server to be fully started
+        _new/src/unison -ui text -selftest testr_c socket://127.0.0.1:55443/testr_s -killserver
+
+    - name: "2.48: Run self-tests over RPC - prev client to new server"
+      if: contains(matrix.job.ref, '2.48')
+      env:
+        UNISON: test2
+      shell: bash
+      run: |
+        cp _new/src/unison _new/src/unison-2.48
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./_new/src/testbak_s _new/src/unison-2.48 -socket 55443 &
+        sleep 1 # Wait for the server to be fully started
+        _prev/src/unison -ui text -selftest testr_c socket://127.0.0.1:55443/testr_s -killserver
+
+    - name: Run self-tests over RPC - prev client to new server
+      if: ${{ !contains(matrix.job.ref, '2.48') }}
+      env:
+        UNISON: test2
+      shell: bash
+      run: |
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./_new/src/testbak_s _new/src/unison -socket 55443 &
+        sleep 1 # Wait for the server to be fully started
+        _prev/src/unison -ui text -selftest testr_c socket://127.0.0.1:55443/testr_s -killserver
+
+
+  ## We know the code is ok with various ocaml versions, so this is
+  ## just checking the dune build process.  Therefore build each OS
+  ## family just once.  Pick a different ocaml version because that's
+  ## better coverage without adding a build.
+  opam_dune_build:
+    strategy:
+      fail-fast: false
+      matrix:
+        job:
+        - { os: ubuntu-22.04   , ocaml-compiler: 4.12.x }
+        - { os: macos-11       , ocaml-compiler: 4.11.x }
+
+    runs-on: ${{ matrix.job.os }}
+
+    steps:
+    - if: contains(matrix.job.os, 'ubuntu')
+      run: sudo apt-get update
+
+    - name: Checkout code
+      uses: actions/checkout@v3
+
+    - name: Use OCaml ${{ matrix.job.ocaml-compiler }}
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: "${{ matrix.job.ocaml-compiler }}"
+
+    - run: opam install . --deps-only
+
+    - run: opam exec -- dune build && cp -L ./_build/install/default/bin/unison* ./src/
+
+#    - run: opam exec -- make test
+
+  bytecode_build:
+    strategy:
+      fail-fast: false
+      matrix:
+        job:
+        - { os: ubuntu-22.04   , ocaml-compiler: 4.14.x }
+
+    runs-on: ${{ matrix.job.os }}
+
+    steps:
+    - if: contains(matrix.job.os, 'ubuntu')
+      run: sudo apt-get update
+
+    - name: Checkout code
+      uses: actions/checkout@v3
+
+    - name: Use OCaml ${{ matrix.job.ocaml-compiler }}
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: "${{ matrix.job.ocaml-compiler }}"
+        opam-pin: false
+        opam-depext: false
+
+    - run: opam exec -- make src UISTYLE=text NATIVE=false
+
+    - run: opam exec -- make test
+
+
+  build_compat:
+    if: ${{ !cancelled() }}    # Don't fail if 'docs' failed
+    needs: docs
+
+    strategy:
+      fail-fast: false
+      matrix:
+        job:
+        - { ocaml-version: 4.14.x, publish: true, fnsuffix: -ubuntu-x86_64 }
+        - { ocaml-version: "ocaml-variants.4.14.1+options,ocaml-option-musl,ocaml-option-static,ocaml-option-flambda", publish: true, fnsuffix: -ubuntu-x86_64-static }
+        - { ocaml-version: 4.13.x }
+        - { ocaml-version: 4.12.x }
+        - { ocaml-version: 4.11.x }
+        - { ocaml-version: 4.10.x }
+        - { ocaml-version: 4.09.x }
+        - { ocaml-version: 4.08.x, publish: true, fnsuffix: +ocaml4.08-ubuntu-x86_64 }
+
+    runs-on: ubuntu-latest
+    container: ubuntu:16.04
+
+    steps:
+    - name: Set up the OS
+      run: |
+        apt-get update
+        apt-get install --assume-yes git make gcc patch wget bzip2 unzip musl-tools
+
+    - name: Checkout code
+      uses: actions/checkout@v3
+
+    - name: Use OCaml ${{ matrix.job.ocaml-version }}
+      uses: ocaml/setup-ocaml@v2
+      with:
+        ocaml-compiler: ${{ matrix.job.ocaml-version }}
+        opam-disable-sandboxing: true
+        opam-pin: false
+        opam-depext: false
+
+    - name: Build text UI
+      run: |
+        opam exec -- make src UISTYLE=text STATIC=${{ contains(matrix.job.ocaml-version, '-musl') }}
+        mkdir -p pkg/bin
+        cp src/unison pkg/bin/
+        cp src/unison-fsmonitor pkg/bin/
+
+    - name: Run local tests
+      run: opam exec -- make test
+
+    - name: Run remote tests
+      run: |
+        mkdir localsocket
+        chmod 700 localsocket
+        # Separate backup dir must be set for server instance so that the central
+        # backup location of both instances doesn't overlap
+        UNISONBACKUPDIR=./src/testbak4 ./src/unison -socket ./localsocket/test.sock &
+        sleep 1 # Wait for the server to be fully started
+        test -S ./localsocket/test.sock
+        ./src/unison -ui text -selftest testr3 socket://{./localsocket/test.sock}/testr4 -killserver
+
+    - name: Build GUI
+      if: ${{ !contains(matrix.job.ocaml-version, '-musl') }}
+      run: |
+        opam depext --install --verbose --yes lablgtk3 && opam install ocamlfind
+        opam exec -- make src UISTYLE=gtk3
+        cp src/unison pkg/bin/unison-gui
+
+    - name: Initialize packaging variables
+      id: vars
+      run: |
+        REF_SHAS=$(echo '${{ github.sha }}' | awk '{ print substr($0, 1, 8) }')
+        unset REF_TAG ; case "${GITHUB_REF}" in refs/tags/*) REF_TAG="${GITHUB_REF#refs/tags/}" ;; esac;
+        PKG_VER="${REF_TAG:-git_$REF_SHAS}"
+        PKG_VER="${PKG_VER#v}"
+        echo PKG_NAME="${PROJECT_NAME}-${PKG_VER}${{ matrix.job.fnsuffix }}.tar.gz" >> $GITHUB_OUTPUT
+        echo REF_SHAS=${REF_SHAS} >> $GITHUB_OUTPUT
+
+    - uses: actions/upload-artifact@v3
+      with:
+        name: unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.ubuntu.x86_64
+        path: pkg/bin/*
+
+    - name: Copy user manual
+      if: matrix.job.publish
+      continue-on-error: ${{ !(github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && matrix.job.publish) }}
+      uses: actions/download-artifact@v3
+      with:
+        name: unison-docs
+        path: pkg
+
+    - name: Prepare package
+      if: matrix.job.publish
+      run: |
+        strip pkg/bin/*
+        cp README* pkg/
+        cp LICENSE* pkg/
+
+    - name: Package
+      if: matrix.job.publish
+      run: cd pkg && tar czf '${{ steps.vars.outputs.PKG_NAME }}' *
+
+    - uses: actions/upload-artifact@v3
+      if: matrix.job.publish
+      with:
+        name: ${{ steps.vars.outputs.PKG_NAME }}.ocaml-${{ matrix.job.ocaml-version }}.ubuntu_compat-publish
+        path: pkg/${{ steps.vars.outputs.PKG_NAME }}
+
+    - name: Publish
+      if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') && matrix.job.publish
+      uses: softprops/action-gh-release@v1
+      with:
+        files: pkg/${{ steps.vars.outputs.PKG_NAME }}
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml
deleted file mode 100644
index f79fe1c..0000000
--- a/.github/workflows/CICD.yml
+++ /dev/null
@@ -1,281 +0,0 @@
-name: CICD
-
-env:
-  PROJECT_NAME: unison
-  PROJECT_DESC: "`unison` file synchronizer"
-  PROJECT_EXES: "unison unison-fsmonitor"
-
-on:
-  - pull_request
-  - push
-
-jobs:
-  build:
-    strategy:
-      fail-fast: false
-      matrix:
-        job:
-        - { os: macos-10.15   , ocaml-version: 4.12.0 }
-        - { os: macos-10.15   , ocaml-version: 4.11.2 }
-        - { os: macos-10.15   , ocaml-version: 4.10.2 }
-        - { os: macos-10.15   , ocaml-version: 4.09.1 }
-        - { os: macos-10.15   , ocaml-version: 4.08.1 }
-        - { os: macos-10.15   , ocaml-version: 4.07.1 }
-        - { os: macos-10.15   , ocaml-version: 4.06.1 }
-        - { os: macos-10.15   , ocaml-version: 4.05.0 }
-        - { os: macos-10.15   , ocaml-version: 4.04.2 }
-        - { os: macos-10.15   , ocaml-version: 4.03.0 }
-        - { os: macos-10.15   , ocaml-version: 4.02.3 }
-        - { os: macos-10.15   , ocaml-version: 4.01.0 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.12.0 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.11.2 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.10.2 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.10.0+musl+static+flambda }
-        - { os: ubuntu-18.04  , ocaml-version: 4.09.1 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.08.1 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.07.1 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.06.1 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.05.0 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.04.2 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.03.0 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.02.3 }
-        - { os: ubuntu-18.04  , ocaml-version: 4.01.0 }
-        - { os: windows-latest , ocaml-version: 4.12.0+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.11.2+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.10.2+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.10.2+mingw32c }
-        - { os: windows-latest , ocaml-version: 4.09.1+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.08.1+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.07.1+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.06.1+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.06.1+mingw32c }
-        - { os: windows-latest , ocaml-version: 4.05.0+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.04.2+mingw32c }
-        - { os: windows-latest , ocaml-version: 4.03.0+mingw32c }
-        - { os: windows-latest , ocaml-version: 4.02.3+mingw64c }
-        - { os: windows-latest , ocaml-version: 4.02.3+mingw32c }
-        - { os: windows-latest , ocaml-version: 4.01.0+mingw32c }
-
-    runs-on: ${{ matrix.job.os }}
-
-    steps:
-    - name: Checkout code
-      uses: actions/checkout@v2
-
-    - name: Initialize workflow variables
-      id: vars
-      shell: bash
-      run: |
-        outputs() { for var in "$@" ; do echo steps.vars.outputs.${var}="${!var}"; echo ::set-output name=${var}::${!var}; done; }
-        # normalize to pre-compiled ocaml compiler variants for windows/Cygwin (decreases OCaml install time by 50%)
-        case '${{ matrix.job.ocaml-version }}' in
-          *+*) OCAML_COMPILER='ocaml-variants.${{ matrix.job.ocaml-version }}' ;;
-            *) OCAML_COMPILER='ocaml-base-compiler.${{ matrix.job.ocaml-version }}' ;;
-        esac
-        OCAML_VARIANT='${{ matrix.job.ocaml-version }}'
-        outputs OCAML_VARIANT OCAML_COMPILER
-        # architecture/platform vars
-        EXE_suffix='' ; case '${{ matrix.job.os }}' in windows-*) EXE_suffix=".exe" ;; esac
-        MinGW_ARCH='x86_64' ; case '${{ matrix.job.ocaml-version }}' in *+mingw32*) MinGW_ARCH='i686' ;; *+mingw64*) MinGW_ARCH='x86_64' ;; esac
-        MSVC_ARCH='' ; case '${{ matrix.job.ocaml-version }}' in *+msvc32*) MSVC_ARCH='x86' ;; *+msvc64*) MSVC_ARCH='x64' ;; esac
-        STATIC='false' ; case '${{ matrix.job.ocaml-version }}' in *+musl*) STATIC='true' ;; esac
-        outputs EXE_suffix MinGW_ARCH MSVC_ARCH STATIC
-        case '${{ matrix.job.os }}' in macos-*) echo "MACOSX_DEPLOYMENT_TARGET=10.6" >> $GITHUB_ENV ;; esac
-        # staging environment
-        STAGING_DIR='_staging'
-        outputs STAGING_DIR
-        # parse commit reference info
-        echo GITHUB_REF=${GITHUB_REF}
-        echo GITHUB_SHA=${GITHUB_SHA}
-        REF_NAME="${GITHUB_REF#refs/*/}"
-        unset REF_BRANCH ; case "${GITHUB_REF}" in refs/heads/*) REF_BRANCH="${GITHUB_REF#refs/heads/}" ;; esac;
-        unset REF_TAG ; case "${GITHUB_REF}" in refs/tags/*) REF_TAG="${GITHUB_REF#refs/tags/}" ;; esac;
-        REF_SHAS="${GITHUB_SHA:0:8}"
-        outputs REF_BRANCH REF_NAME REF_SHAS REF_TAG
-        # deployable tag? (ie, leading "vM" or "M"; M == version number)
-        unset DEPLOY ; if [[ $REF_TAG =~ ^[vV]?[0-9].* ]]; then DEPLOY='true' ; fi
-        outputs DEPLOY
-        # package name
-        PKG_suffix='.tar.gz' ; case '${{ matrix.job.os }}' in windows-*) PKG_suffix='.zip' ;; esac;
-        PKG_OS='linux' ; case '${{ matrix.job.os }}' in macos-*) PKG_OS='${{ matrix.job.os }}' ;; windows-*) PKG_OS='windows' ;; esac;
-        PKG_STATIC='' ; case '${{ matrix.job.ocaml-version }}' in *+static*) PKG_STATIC='.static' ;; esac;
-        PKG_ARCH='x86_64' ; case '${{ matrix.job.ocaml-version }}' in *+32bit* | *+mingw32*) PKG_ARCH='i386' ;; esac;
-        PKG_VER="${REF_TAG:-$REF_SHAS}"
-        PKG_BASENAME="${PROJECT_NAME}-${PKG_VER}+ocaml-${OCAML_VARIANT/%+*/}+${PKG_ARCH}.${PKG_OS}${PKG_STATIC}"
-        PKG_NAME="${PKG_BASENAME}${PKG_suffix}"
-        PKG_DIR="${STAGING_DIR}/${PKG_BASENAME}"
-        outputs PKG_VER PKG_BASENAME PKG_DIR PKG_NAME PKG_OS PKG_suffix
-
-    - name: Create/configure any needed build/workspace
-      shell: bash
-      run: |
-        # create build/work space
-        mkdir -p '${{ steps.vars.outputs.STAGING_DIR }}'
-        mkdir -p '${{ steps.vars.outputs.PKG_DIR }}'
-        mkdir -p '${{ steps.vars.outputs.PKG_DIR }}'/bin
-
-    - name: Select Xcode version 11.7 for OCaml 4.05 (macOS)
-      if: runner.os == 'macOS' && matrix.job.ocaml-version == '4.05.0'
-      ## Xcode >= 12 breaks building lablgtk with OCaml 4.05 (a bug fixed in OCaml >= 4.06)
-      run: |
-        sudo xcode-select -s "/Applications/Xcode_11.7.app"
-
-    - name: Enable/config MSVC environment (if/when needed)
-      uses: ilammy/msvc-dev-cmd@v1
-      with:
-        arch: "${{ steps.vars.outputs.MSVC_ARCH }}"
-      if: contains(matrix.job.ocaml-version, '+msvc')
-
-    - name: Use OCaml ${{ matrix.job.ocaml-version }}
-      uses: ocaml/setup-ocaml@v2
-      with:
-        ocaml-compiler: "${{ steps.vars.outputs.OCAML_COMPILER }}"
-        opam-pin: false
-        opam-depext: false
-      # setup-ocaml can prepare the build environment from unison.opam
-      # We're not relying on that capability here, to make sure the builds
-      # also work without using unison.opam
-
-    ## note: at this point, after OCaml installation, windows platforms will use Cygwin bash as the default
-    ##   ... Cygwin bash cannot handle shell scripts containing CRLF EOLs (which are what is generated by GHA on windows platforms)
-    ##   ... so, "igncr" must be added to SHELLOPTS
-
-    - name: Prepare Cygwin environment (Windows)
-      if: runner.os == 'Windows'
-      shell: cmd
-      run: |
-        echo %CYGWIN_ROOT_BIN%>> %GITHUB_PATH%
-        echo %CYGWIN_ROOT_WRAPPERBIN%>> %GITHUB_PATH%
-        echo SHELLOPTS=igncr>> %GITHUB_ENV%
-
-    - name: lablgtk install
-      ## [2020-09] non-working/unavailable for MSVC or musl OCaml variants ; also, non-working for 32bit OCaml variant (see [GH:garrigue/lablgtk#64](https://github.com/garrigue/lablgtk/issues/64))
-      if: ${{ ! ( contains(matrix.job.ocaml-version, '+msvc') || contains(matrix.job.ocaml-version, '+musl') || contains(matrix.job.ocaml-version, '+32bit') ) }}
-      run: opam depext --install --verbose --yes lablgtk
-
-    - shell: bash
-      run: |
-        opam exec -- make src OSTYPE=$OSTYPE UISTYLE=text STATIC=${{ steps.vars.outputs.STATIC }}
-        # stage
-        # * notes: darwin/macos doesn't build `unison-fsmonitor`
-        for file in ${PROJECT_EXES} ; do
-          if [ -f "src/${file}${{ steps.vars.outputs.EXE_suffix }}" ]; then
-            cp "src/${file}${{ steps.vars.outputs.EXE_suffix }}" '${{ steps.vars.outputs.PKG_DIR }}/bin'
-            echo "'src/${file}${{ steps.vars.outputs.EXE_suffix }}' copied to '${{ steps.vars.outputs.PKG_DIR }}/bin'"
-          fi
-        done
-
-    - run: opam exec -- make test
-
-    - if: steps.vars.outputs.STATIC != 'true' ## unable to build static gtk for linux or windows/Cygwin MinGW platforms
-      shell: bash
-      run: |
-        opam exec -- make src OSTYPE=$OSTYPE UISTYLE=gtk2 STATIC=${{ steps.vars.outputs.STATIC }}
-        # stage
-        # * copy only main/first project binary
-        project_exe_stem=${PROJECT_EXES%% *}
-        cp "src/${project_exe_stem}${{ steps.vars.outputs.EXE_suffix }}" "${{ steps.vars.outputs.PKG_DIR }}/bin/${project_exe_stem}-gtk2${{ steps.vars.outputs.EXE_suffix }}"
-
-    - uses: actions/upload-artifact@v2
-      with:
-        name: unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}
-        path: ${{ steps.vars.outputs.PKG_DIR }}/bin/*
-
-    - name: Package
-      # if: steps.vars.outputs.DEPLOY
-      shell: bash
-      run: |
-        ## package artifact(s)
-        PKG_DIR='${{ steps.vars.outputs.PKG_DIR }}'
-        # `strip` binaries
-        strip "${PKG_DIR}/bin"/*'${{ steps.vars.outputs.EXE_suffix }}'
-        # README and LICENSE
-        (shopt -s nullglob; for f in [R]'EADME'{,.*}; do cp $f "${PKG_DIR}"/ ; done)
-        (shopt -s nullglob; for f in [L]'ICENSE'{-*,}{,.*}; do cp $f "${PKG_DIR}"/ ; done)
-        # collect any needed dlls/libraries
-        case '${{ matrix.job.os }}' in
-          windows-*)
-            # dlls
-            dll_refs() { eval "$(opam env)" ; eval "$(ocaml-env cygwin)" ; objdump -x "$@" | grep -Po "\S+[.]dll$" | xargs -I{} 2>/dev/null which "{}" | sort -u ; }
-            filtered_dll_refs() { list="$(dll_refs "$@" | grep -vF "$(cygpath ${WINDIR})" | perl -lape '$_ = qq/@{[sort @F]}/')" ; echo "$list" ; }
-            recursive_filtered_dll_refs() { list="$(filtered_dll_refs "$@")" ; n=0 ; while [ $n -lt $(echo "$list" | wc -l) ]; do n=$(echo "$list" | wc -l) ; list="$(filtered_dll_refs $list)" ; done ; echo "$list" ; }
-            IFS=$'\n' DLL_list=( "$(recursive_filtered_dll_refs "${PKG_DIR}"/bin/*)" )
-            for dll in ${DLL_list[@]} ; do cp "${dll}" "${PKG_DIR}"/bin ; done
-            TARGET_ARCH_ID='x86_64'; case '${{ matrix.job.ocaml-version }}' in *+mingw32*|*+msvc32*) TARGET_ARCH_ID='i686' ;; esac
-            # required gdk support files
-            mkdir "${PKG_DIR}"/lib
-            cp -r /usr/${TARGET_ARCH_ID}-w64-mingw32/sys-root/mingw/lib/gdk-pixbuf-2.0 "${PKG_DIR}"/lib/
-            # update loader.cache to point to local relative installation
-            mv "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original
-            cat "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original | sed -E 's#([^"]*)(lib/gdk-pixbuf-2.0/2.10.0/loaders/[^"]*[.]dll)#../\2#' > "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
-            rm "${PKG_DIR}"/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache.original
-            ;;
-        esac
-        # create compressed package
-        pushd "${PKG_DIR}"/ >/dev/null
-        case '${{ matrix.job.os }}' in
-          windows-*) 7z -y a '../${{ steps.vars.outputs.PKG_NAME }}' * | tail -2 ;;
-          *) tar czf '../${{ steps.vars.outputs.PKG_NAME }}' * ;;
-        esac
-        popd >/dev/null
-
-    - name: Publish
-      if: steps.vars.outputs.DEPLOY
-      uses: softprops/action-gh-release@v1
-      with:
-        files: |
-          ${{ steps.vars.outputs.STAGING_DIR }}/${{ steps.vars.outputs.PKG_NAME }}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-    - if: runner.os == 'macOS'
-      name: "macOS: Build and package Unison.app"
-      id: macapp
-      run: |
-        opam exec -- make src UISTYLE=mac
-
-        # package
-        APP_NAME=Unison-${{ steps.vars.outputs.PKG_VER }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}.app.tar.gz
-        echo ::set-output name=APP_NAME::${APP_NAME}
-
-        tar czf ${APP_NAME} -C src/uimac/build/Default Unison.app
-
-    - if: runner.os == 'macOS'
-      name: "macOS: Upload Unison.app artifact"
-      uses: actions/upload-artifact@v2
-      with:
-        name: Unison-${{ steps.vars.outputs.REF_SHAS }}.ocaml-${{ matrix.job.ocaml-version }}.${{ matrix.job.os }}.app
-        path: ${{ steps.macapp.outputs.APP_NAME }}
-
-    - if: runner.os == 'macOS' && steps.vars.outputs.DEPLOY
-      name: "macOS: Publish Unison.app"
-      uses: softprops/action-gh-release@v1
-      with:
-        files: ${{ steps.macapp.outputs.APP_NAME }}
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-  opam_dune_build:
-    strategy:
-      fail-fast: false
-      matrix:
-        job:
-        - { os: ubuntu-latest  , ocaml-compiler: 4.12.x }
-        - { os: macos-latest  , ocaml-compiler: 4.12.x }
-
-    runs-on: ${{ matrix.job.os }}
-
-    steps:
-    - name: Checkout code
-      uses: actions/checkout@v2
-
-    - name: Use OCaml ${{ matrix.job.ocaml-compiler }}
-      uses: ocaml/setup-ocaml@v2
-      with:
-        ocaml-compiler: "${{ matrix.job.ocaml-compiler }}"
-
-    - run: opam install . --deps-only
-
-    - run: opam exec -- dune build && cp -L ./_build/install/default/bin/unison* ./src/
-
-#    - run: opam exec -- make test
diff --git a/.gitignore b/.gitignore
index 7bcb4e2..fdc9a9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,6 @@
 /src/ubase/projectInfo.ml
 /src/unison
 /src/unison-fsmonitor
-/src/ubase/depend
 TAGS
 build
 logmsg
@@ -40,13 +39,23 @@ doc/docs
 doc/junk.ps
 doc/postproc
 doc/postproc.ml
+doc/prefs.tmp
+doc/prefsdocs.tmp
+doc/temp.dvi
+doc/temp.html
+doc/texdirectives.tex
 doc/unison-manual.dtxt
 doc/unison-manual.dvi
 doc/unison-manual.html
 doc/unison-manual.htoc
 doc/unison-manual.pdf
 doc/unison-manual.ps
+doc/unison-manual.txt
 doc/unisonversion.tex
+man/*.tmp
+man/unison.1
+src/.depend.dot.tmp
+src/DEPENDENCIES.ps
 
 _build
 .merlin
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 65b1168..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-language: c
-sudo: required
-before_install:
-    - curl -L https://raw.githubusercontent.com/ocaml/ocaml-ci-scripts/master/.travis-ocaml.sh | sh
-    - eval $(opam config env)
-install:
-    make
-script:
-    make test
-
-cache:
-  directories:
-  - $HOME/.opam
-  - /usr/local
-
-env:
-  # - OCAML_VERSION=4.01
-  - OCAML_VERSION=4.08
-
-os:
-  - linux
-#  - osx
-
-# BCP and brabalan, 1/19: We took away the OSX build because it is
-# horribly slow and 98% redundant...
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 202d195..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-FROM debian:stable-slim as builder
-
-RUN set -ex; \
-  apt-get -y update; \
-  apt-get -y install curl build-essential opam
-
-COPY . /usr/src/unison
-
-RUN set -ex; \
-  cd /usr/src/unison; \
-  make
-
-FROM debian:stable-slim
-COPY --from=builder /usr/src/unison/src/unison* /usr/local/bin/
-
-ENTRYPOINT ["unison"]
-CMD ["-doc", "about"]
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 0000000..0bda622
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,139 @@
+# Installing Unison
+
+## Pre-built binaries
+
+The easiest and quickest way is to install pre-built binaries available at many
+package repositories. The caveat: some repositories are slow to update and
+often include a very old version of Unison.
+
+Alternatively, some pre-built binaries are made available at
+https://github.com/bcpierce00/unison/releases for macOS, Linux and Windows.
+
+
+## Building from source
+
+### Package repositories
+
+Many package repositories, including source-based repositories like `pkgsrc`,
+make it easy to build from source by handling all the dependencies for you.
+Please refer to instructions provided by the repository.
+
+
+### Unix-like OS (GNU/Linux, BSDs, macOS, illumos-based OS, Solaris, ...)
+
+#### Build prerequisites
+
+- A recent version of OCaml compiler (version 4.08 at minimum) together with a
+  C99 compiler (such as gcc, clang) -- see https://ocaml.org/
+- GNU make
+- Basic POSIX tools: sh, sed (optional, for manuals)
+
+##### Optional, for the GUI only
+
+- lablgtk3 and its prerequisites (ocamlfind, GTK 3 and its dependencies)
+
+##### Optional, for building the user manual
+
+- LaTeX
+- dvips
+- ps2pdf (included with Ghostscript, for example)
+- HEVEA (https://hevea.inria.fr/) (optional, for HTML and text formats)
+- Lynx (optional, for text format)
+- (for developers only) HEVEA and Lynx are required to re-build the manual
+  included in the Unison binary
+
+#### Building
+
+Building from source is as simple as executing:
+```
+make
+```
+
+Use `gmake` in environments where GNU make is not the default. If you are
+using OPAM then `opam exec -- make` may work for you, as opam needs to set up
+a specific environment.
+
+Presence of lablgtk3 is detected automatically. If you want to force building
+the GUI (or not), type `make UISTYLE=gtk3` or `make UISTYLE=text`.
+
+There is currently no installation provided by the makefile. You can just copy
+the built binaries to where you need them. The following files are produced:
+```
+src/unison              (the main executable)
+src/unison-fsmonitor    (optional, on some build platforms)
+src/fsmonitor.py        (optional, if unison-fsmonitor is not built)
+man/unison.1            (optional, manual page)
+doc/unison-manual.*     (optional, user manual in different formats)
+```
+
+#### Building all from scratch
+
+It is very easy to build both the OCaml compiler and Unison from source without
+using a package repository. Building OCaml has very few prerequisites. If your
+system has a supported C compiler installed then the following may work out of
+the box:
+
+```
+# Build OCaml compiler
+## In a dir with extracted OCaml source
+./configure
+make
+make install    # you may need elevated privileges
+
+# Build unison
+## In a dir with extracted Unison source
+make
+```
+
+Building the GTK GUI this way is difficult as both GTK 3 and lablgtk3 must be
+built and installed.
+
+
+### macOS
+
+#### Build prerequisites
+
+- Xcode Command Line Tools (optional, for the native GUI)
+
+#### Building
+
+For the text user interface and GTK GUI, follow the Unix instructions above.
+
+To build the macOS native GUI, execute:
+```
+make UISTYLE=mac
+```
+
+The built application will be located at `src/uimac/build/Default/Unison.app`.
+
+
+### Windows
+
+Building on Windows is currently somewhat complicated. All methods require
+Cygwin as a POSIX-like layer for Windows. Cygwin is required for the build
+process only; the build can produce fully native Windows binaries that don't
+require Cygwin to run.
+
+Builds are possible with MS Visual C++ (MSVC) (currently untested and likely
+not working), MinGW-w64 (currently the best option) and Cygwin GNU C (untested;
+produced binaries require Cygwin to run).
+
+Tradeoff?
+
+- MSVC and MinGW can produce statically linked Unison executable.
+- The Cygwin GNU C option requires only free software.
+
+You have to add `OSTYPE=$OSTYPE` as argument to `make`: `make OSTYPE=$OSTYPE`
+(OSTYPE is preset to 'cygwin' by Cygwin).
+
+
+### Build options
+
+There are some additional options that control the build process:
+
+- NATIVE: If you can't compile a native binary for your platform then add
+  `NATIVE=false` as argument to `make`. This will produce a single native
+  executable with OCaml runtime and Unison bytecode embedded.
+- STATIC: Adding `STATIC=true` as argument to `make` will produce a (mostly)
+  statically linked executable. This may not work on all platforms or with all
+  build methods.
diff --git a/Makefile b/Makefile
index d5f38f5..1f962d4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,230 +1,50 @@
-.PHONY: all src
+# Unison file synchronizer: Makefile
+# See LICENSE for terms.
 
-default: text
-
-text:
-	$(MAKE) -C src UISTYLE=text
-
-test:
-	./src/unison -ui text -selftest
-
-all: src
+.PHONY: all
+all: src manpage
 
+.PHONY: src
 src:
 	$(MAKE) -C src
 
--include src/Makefile.ProjectInfo
-
+# It's a wart that docs need "unison" built (vs some docs utility).
+# Having docs build src/unison points out that UISTYLE is a bug; either
+# docs might build the GUI (not wanted as too heavy for docs use), or
+# whatever is built might not be rebuilt later.
+.PHONY: docs
 docs:
 	$(MAKE) -C src UISTYLE=text
 	$(MAKE) -C doc
+	$(MAKE) -C man
 
-include src/Makefile.OCaml
-
-######################################################################
-# Export
-
-ifeq ($(OSARCH),win32)
-  BCPHOME=/cygdrive/s
-  # BCPHOME=/home/exporting
-  EXPORTNATIVE=true
-  EXPORTSTATIC=true
-else
-ifeq ($(OSARCH),win32gnuc)
-  BCPHOME=/home/exporting
-  EXPORTNATIVE=true
-  EXPORTSTATIC=false
-else
-ifeq ($(OSARCH),linux)
-  EXPORTNATIVE=true
-  EXPORTSTATIC=false
-else
-ifeq ($(OSARCH),osx)
-  EXPORTNATIVE=true
-  EXPORTSTATIC=false
-else # Solaris
-  EXPORTNATIVE=true
-  EXPORTSTATIC=true
-endif
-endif
-  BCPHOME=$(HOME)
-endif
-endif
-
-EXPORTDIR=$(BCPHOME)/pub/$(NAME)
-DOWNLOADAREA=releases
-DOWNLOADPARENT=$(EXPORTDIR)/download/$(DOWNLOADAREA)
-REALDOWNLOADDIR=$(DOWNLOADPARENT)/$(NAME)-$(VERSION)
-BRANCH=$(MAJORVERSION)
-EXPORTNAME=$(NAME)-$(VERSION)
-# OSX/linux portability
-ifeq ($(OSARCH),osx)
-	TMP=$(shell mktemp -d -t unison)
-else
-	TMP=$(shell mktemp -d)
-endif
-DOWNLOADDIR=/tmp/$(NAME)-$(VERSION)
-# DOWNLOADDIR=$(REALDOWNLOADDIR)
-
-# OLD
-# Do this when it's time to create a new beta-release from the development trunk
-#beta: 
-#	@echo "Makefile needs fixing"
-#	@exit 1
-#	@tools/ask tools/exportmsg.txt
-#	(cd ..; svn copy trunk branches/$(BRANCH))
-#	(cd ../branches/$(BRANCH); svn commit -m "New release branch")
-#	@echo
-#	@echo "Press RETURN to export it... "
-#	@read JUNK
-#	$(MAKE) -C ../branches/$(BRANCH) export
-
-# Do this in a release branch to export a new tarball (e.g., after fixing a bug)
-# (builds a beta release)
-export:
-	@echo
-	@echo "CHECKLIST:"
-	@echo "  - Bump minor version number in src/Makefile.ProjectInfo"
-	@echo "  - Move everything interesting from src/RECENTNEWS to doc/changes.tex"
-	@echo ""
-	@echo "If all this is done, hit RETURN (otherwise Ctrl-C and do it)"
-	@read JUNK
-	$(MAKE) $(DOWNLOADDIR)
-	$(MAKE) exportdocs
-	$(MAKE) exportsources
-	(cd $(DOWNLOADDIR); genindexhtml)
-	@echo
-	@echo "OK to commit?  Press RETURN if yes (Crtl-C if no)..."
-	@read JUNK
-	$(MAKE) commitexport
-
-commitexport:
-	$(MAKE) realcommit
-	$(MAKE) mailchanges
-
-realcommit:
-	@echo
-	@echo Committing new export directory
-	mv $(DOWNLOADDIR) $(REALDOWNLOADDIR)
-	-chmod -R a+r $(EXPORTDIR)
-	-chmod -R g+wr $(EXPORTDIR)
-	-chmod -R o-w $(EXPORTDIR)
-	-$(RM) $(DOWNLOADPARENT)/beta
-	-ln -s $(EXPORTNAME) $(DOWNLOADPARENT)/beta
-	(cd $(DOWNLOADPARENT); genindexhtml)
-
-$(DOWNLOADDIR):
-	@echo Creating DOWNLOADDIR = $(DOWNLOADDIR)
-	@echo
-	-mkdir -p $(DOWNLOADDIR)
-
-exportsources:
-	git archive --output $(DOWNLOADDIR)/$(EXPORTNAME).tar.gz -- HEAD src
-
-exportdocs:
-	-rm -f src/unison
-	$(MAKE) -C src UISTYLE=text DEBUGGING=false \
-                       NATIVE=$(EXPORTNATIVE) STATIC=$(EXPORTSTATIC)
-	-$(RM) src/strings.ml
-	$(MAKE) -C doc TEXDIRECTIVES+="\\\\draftfalse" real
-	$(MAKE) -C src UISTYLE=text DEBUGGING=false \
-                       NATIVE=$(EXPORTNATIVE) STATIC=$(EXPORTSTATIC)
-	src/unison -doc news > src/NEWS
-	cp doc/unison-manual.ps $(DOWNLOADDIR)/$(EXPORTNAME)-manual.ps
-	-cp doc/unison-manual.pdf $(DOWNLOADDIR)/$(EXPORTNAME)-manual.pdf
-	cp doc/unison-manual.html $(DOWNLOADDIR)/$(EXPORTNAME)-manual.html
-	cp doc/unison-manual.html $(DOWNLOADDIR)/$(NAME)-manual.html
+# "src" is a prerequisite to prevent parallel build errors.
+# manpage builds currently require a pre-built "unison" binary.
+.PHONY: manpage
+manpage: src
+	$(MAKE) -C man
 
-MAILTMP = $(HOME)/mail.tmp
-
-mailchanges: 
-	@echo To: $(NAME)-announce@yahoogroups.com,$(NAME)-users@yahoogroups.com \
-            > $(MAILTMP)
-	@echo Subject: $(NAME) $(VERSION) now available >> $(MAILTMP)
-	@echo >> $(MAILTMP)
-	@echo Download address: >> $(MAILTMP)
-	@echo "  " http://www.cis.upenn.edu/~bcpierce/unison/download.html \
-           >> $(MAILTMP)
-	@echo >> $(MAILTMP)
-	@cat src/NEWS >> $(MAILTMP)
-	@src/unison -doc news >> $(MAILTMP)
-	@echo "Announcement draft can be found in $(MAILTMP)"
-
-######################################################################
-# Export binary for the current architecture
-# (this stuff is all probably dead)
-
-EXPORTTMP=$(TMP)/export-$(OSARCH)x.tmp
-
-exportnative:
-	-$(RM) -r $(EXPORTTMP)
-	cp -r src $(EXPORTTMP)
-	$(MAKE) realexportnative
-ifeq ($(OSARCH),linux)
-	$(MAKE) realexportnative EXPORTSTATIC=true KIND=-static
-endif
-	$(RM) -r $(EXPORTTMP)
-
-realexportnative:
-	-$(MAKE) -C $(EXPORTTMP) clean
-	$(MAKE) -C $(EXPORTTMP) UISTYLE=text DEBUGGING=false \
-                       NATIVE=$(EXPORTNATIVE) STATIC=$(EXPORTSTATIC)
-	-mkdir -p $(DOWNLOADDIR)
-	cp $(EXPORTTMP)/$(NAME)$(EXEC_EXT) \
-               $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-textui$(EXEC_EXT)
-	gzip --best --force -c \
-            $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-textui$(EXEC_EXT) \
-          > $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-textui$(EXEC_EXT).gz
-	$(MAKE) -C $(EXPORTTMP) UISTYLE=gtk2 DEBUGGING=false \
-                       NATIVE=$(EXPORTNATIVE) STATIC=$(EXPORTSTATIC)
-	cp $(EXPORTTMP)/$(NAME)$(EXEC_EXT) \
-               $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-gtkui$(EXEC_EXT)
-	gzip --best --force -c \
-            $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-gtkui$(EXEC_EXT) \
-          > $(DOWNLOADDIR)/$(EXPORTNAME).$(OSARCH)$(KIND)-gtkui$(EXEC_EXT).gz
-
-
-######################################################################
-# Version control
-
-checkin: logmsg remembernews
-	git commit -a --file=logmsg
-	$(RM) logmsg
-	@echo 
-	@echo "Remember to do:"
-	@echo "   git pull && git push"
-
-remembernews: logmsg
-	echo "CHANGES FROM VERSION" $(VERSION) > rc.tmp
-	echo >> rc.tmp
-	cat logmsg >> rc.tmp
-	echo  >> rc.tmp
-	echo    ------------------------------- >> rc.tmp
-	-cat src/RECENTNEWS >> rc.tmp
-	mv -f rc.tmp src/RECENTNEWS
-
-######################################################################
-# Misc
+.PHONY: test
+test:
+	./src/unison -ui text -selftest
 
-depend::
+.PHONY: depend
+depend:
 	$(MAKE) -C src depend
 
-clean::
-	$(RM) -r *.tmp \
-	   *.o *.obj *.cmo *.cmx *.cmi core TAGS *~ *.log \
-	   *.aux *.log *.dvi *.out *.backup[0-9] *.bak $(STABLEFLAG)
+.PHONY: clean
+clean:
 	$(MAKE) -C doc clean
+	$(MAKE) -C man clean
 	$(MAKE) -C src clean
 
+.PHONY: install
 install:
-	(cd src; $(MAKE) install)
-
-installtext:
-	(cd src; $(MAKE) install UISTYLE=text)
-
-src/$(NAME):
-	$(MAKE) -C src
-
-windres:
-	windres src/win32rc/unison.rc -O coff src/win32rc/unison.res.lib
-	windres src/win32rc/unison.rc -O res src/win32rc/unison.res
+	@printf "\n\n=========================================\n\
+To install, copy the files src/unison, src/unison-gui (optional),\n\
+src/unison-fsmonitor (optional) and src/fsmonitor.py (optional,\n\
+if unison-fsmonitor does not exist) to a freely chosen location.\n\n\
+Manual page is at man/unison.1 and user manual is at\n\
+doc/unison-manual.pdf, doc/unison-manual.html and doc/unison-manual.txt\n\
+=========================================\n\n\n"
+	@exit 1
diff --git a/src/NEWS b/NEWS.md
similarity index 95%
rename from src/NEWS
rename to NEWS.md
index fc5ac2d..f5a329e 100644
--- a/src/NEWS
+++ b/NEWS.md
@@ -1,7 +1,114 @@
+# UNISON NEWS
 
-Changes in Version 2.51.5
+This file contains a summary of user-visible or important changes, in
+the style of the GNU coding standards.  By user-visible, we include
+changes relevant for those building unison from source.  We omit most
+bugfixes and minor improvements, but of course every release contains
+some.  Refer to the documentation for details; this file is a terse
+notice of changes rather than a tutorial about new features.
+
+As of 2022, this file (NEWS.md at top level) is used for news.
+
+Some software has a "changelog" file that records all changes, but
+unison uses git history for that, and thus there is no changelog file.
+
+## Changes in 2.53.3
+
+Released 2023-04-28
+
+  * On Linux, allow syncing the xattrs where POSIX draft ACLS are
+    stored, which enables syncing the ACL info.  (Note that this does
+    not enable syncing ACLs with the `-acl` switch, and does not
+    enable syncing ACLs between other systems and Linux.  See the
+    manual.)
+  * Improved ETA calculation and sync speed display in text UI.
+  * Fix CI Windows builds (again).
+  * Drop unmaintained "make install" target.
+  * Bugfixes, minor improvements, cleanups.
+
+## Changes in 2.53.2
+
+Released 2023-03-20
+
+  * Change version string to 2.53.2 (2.53.1 identified as 2.53.0).
+
+## Changes in 2.53.1
+
+Released 2023-03-19
+
+  * Repeat mode is more fault tolerant, recovering after temporary
+    errors.
+  * Preferences "force", "prefer" and related no longer require
+    specifying the full root.
+  * Improve stopping of update propagation.
+  * Enable VT input escape codes in Windows.
+  * Respect user-provided CFLAGS, CPPFLAGS, LDFLAGS, LDLIBS.
+  * Add build instructions in INSTALL.md (and drop from manual).
+  * Add graceful stop in repeat mode - SIGUSR2.
+  * Add watch+seconds to 'repeat' preference.
+
+## Changes in 2.53.0
+
+Released 2022-11-07
+
+  * OCaml >= 4.08 is required to build unison.
+  * unison can be built with (unreleased) OCaml 5.
+  * Change GUI to use GTK3 (via lablgtk3) instead of GTK2 (via lablgtk2)
+  * Add support for syncing extended attributes.
+  * Add support for syncing ACLs.
+  * On Windows, add the ability to build unison as a hybrid
+    application (GUI application attached to a text console) by
+    defining UI_WINOS=hybrid (see src/Makefile).  Add this to CI.
+    (Doing this for non-Windows is unnecessary as all applications,
+    both GUI and non-GUI, are always executed with a connection to
+    stdout/stderr. GUI-only applications (ie, no stdout/stderr) is a
+    Windows-only concept.)
+  * Notable bugfixes
+    - Merge results are stored in archive more accurately.
+    - Windows `\\?\` paths now work correctly (including `\\?\Volume{GUID}\` paths).
+  * CI changes
+    - The macOS binaries are properly signed.
+    - Add workaround for bugs in the github CI Windows builds, one of
+      which resulted in the 2.52.1 GUI version failing, in the Windows
+      CI build artifacts.  (This does not affect platforms other than
+      Windows, and may not affect other Windows builds.)
+  * Changes that should not affect behavior
+    - Clean up a variety of unmaintained and unused bits, mainly
+      build-related.
+    - OCaml's Unix library is now extensively used also on
+      Windows. This allowed removal of large amount of
+      Windows-specific OCaml and mainly C code.
+
+## Changes in 2.52.1
+
+Released 2022-05-08
+
+   * Deprecate ocaml < 4.08: 2.53.0 will require 4.08 or higher
+   * Add man page
+   * Remove rsh:// URI scheme (ssh:// of course remains).
+   * Significant bugfixes and minor improvements
+   * Deprecate "backups" preference (see manual for alternatives)
+   * Deprecate "stream" and "halfduplex" preferences
+
+## Changes in 2.52.0
+
+Released 2022-03-12
+
+   * Feature negotiation, compatible with 2.51.
+   * New archive format (independent of ocaml version, based on umarshal)
+     Upgrade is automatic.
+   * New wire protocol (independent of ocaml version, based on umarshal)
+     New protocol is used if both sides are >= 2.52.0.
+   * Compatibility with 2.48
+   * Support for unix-domain sockets
+   * Many bugfixes and minor improvements
+   * ocaml compatibility is now >= 4.01
+   * NEWS is now in NEWS.md and not in the manual
+
+## Changes in 2.51.5
+
+Released 2021-12-18
 
-   Changes since 2.51.4:
      * Restore OCaml compat to before 4.02
      * dune/opam improvements/fixes
      * Improve GTK UI by using GtkTreeView
@@ -9,7 +116,10 @@ Changes in Version 2.51.5
      * Improve ssh support on Windows (hide Windows console in GUI mode)
      * Many bugfixes and minor improvements
 
-   Changes since 2.51.3:
+## Changes in 2.51.4
+
+Released 2021-06-24
+
      * OCaml 4.12 support
      * fsmonitor improvements and Solaris support
      * Color support in text UI, with a new preference, disabled by
@@ -21,7 +131,10 @@ Changes in Version 2.51.5
      * Build cleanups, CI improvements, housekeeping
      * Many bugfixes and minor improvements
 
-   Changes since 2.51.2:
+## Changes in 2.51.3
+
+Released 2020-10-21
+
      * Some nontrivial changes to profile parsing (G.raud Meyer)
           + '=' has been considered whitespace until now: several
             following chars are considered as only one; trailing chars are
@@ -39,7 +152,8 @@ Changes in Version 2.51.5
             etc., plus several more useful key-commands. Type "?" to
             Unison to see all available commands.
 
-   Changes since 2.48:
+## Changes in 2.51.0 through 2.51.2
+
      * Repository transplanted from SVN to Git and moved to GitHub ()
        (https://github.com/bcpierce00/unison).
      * Add a new preference, 'atomic', for specifying directories that
@@ -61,7 +175,8 @@ Changes in Version 2.51.5
           + Added a DockerFile for the convenience of Docker users.
           + Many small bugfixes and UI improvements.
 
-   Changes since 2.45:
+## Changes in 2.48
+
      * Incorporated a patch from Christopher Zimmermann to replace the
        Uprintf module (which doesn't work with OCaml 4.02, causing Unison
        to crash) with equivalent functionality from the standard library.
@@ -103,7 +218,7 @@ Changes in Version 2.51.5
      * Minor:
           + Fixed a bug in export procedure that was messing up
             documentation strings.
-          + Incorporated a patch from Ir�nyossy Knoblauch Art�r to make
+          + Incorporated a patch from Irányossy Knoblauch Artúr to make
             temp file names fit within 143 characters (to make eCryptFS
             happy).
           + Added a string to the Conflict direction to document the
@@ -120,6 +235,11 @@ Changes in Version 2.51.5
           + Fixed Makefile for cross-compiling towards Windows (updated to
             MinGW-w64)
 
+## Changes in very old versions
+
+(Note that these are written 'Changes since' and thus the content
+applies to the release after that.)
+
    Changes since 2.40.63:
      * New preference fastercheckUNSAFE, which can be used (with care!) to
        achieve much faster update detection when all the common files in
@@ -706,7 +826,7 @@ Changes in Version 2.51.5
           + Ignore trailing dots in filenames in case insensitive mode
           + Proper quoting of paths, files and extensions ignored using
             the UI
-          + The strings CURRENT1 and CURRENT2 are now correctly substitued
+          + The strings CURRENT1 and CURRENT2 are now correctly substituted
             when they occur in the diff preference
           + Improvements to syncing resource forks between Macs via a
             non-Mac system.
@@ -1344,7 +1464,7 @@ Changes in Version 2.51.5
             preference is a mask indicating which permission bits should
             be synchronized. It is set by default to 0o1777: all bits but
             the set-uid and set-gid bits are synchronised (synchronizing
-            theses latter bits can be a security hazard). If you want to
+            these latter bits can be a security hazard). If you want to
             synchronize all bits, you can set the value of this preference
             to -1.
           + Added a log preference (default false), which makes Unison
@@ -1446,7 +1566,7 @@ Changes in Version 2.51.5
             paths matching this pattern to be displayed last.
        The sorting preferences are described in more detail in the user
        manual. The sortnewfirst and sortbysize flags can also be accessed
-       from the 'Sort' menu in the grpahical user interface.
+       from the 'Sort' menu in the graphical user interface.
      * Added two new preferences that can be used to change unison's
        fundamental behavior to make it more like a mirroring tool instead
        of a synchronizer.
@@ -1813,5 +1933,3 @@ Changes in Version 2.51.5
        around with.
      * Added a file CONTRIB with some suggestions for how to help us make
        Unison better.
-
-
diff --git a/README.md b/README.md
index f3d80d6..749fe17 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,17 @@
+![Unison](icons/unison.png)
+
 # Unison File Synchronizer
 
-[![Build Status](https://travis-ci.org/bcpierce00/unison.svg?branch=master)](https://travis-ci.org/bcpierce00/unison)
-[![CICD](https://github.com/bcpierce00/unison/workflows/CICD/badge.svg)](https://github.com/bcpierce00/unison/actions?query=workflow%3ACICD)
+## Meta
+
+***Please read this entire README and
+https://github.com/bcpierce00/unison/wiki/Reporting-Bugs-and-Feature-Requests
+before creating or commenting on a github issue.***
+
+***TL;DR: Do not ask questions or ask for help in issues.  Upgrade to the latest release.***
 
-***Please read this entire README before creating or commenting on a
-github issue.  TL;DR: Do not ask questions or ask for help in
-issues.***
+For compatibility information with version 2.52, see
+https://github.com/bcpierce00/unison/wiki/2.52-Migration-Guide
 
 ## About
 
@@ -61,12 +67,19 @@ several points where it differs:
 Note that only a very small number of people are actively working on
 maintaining unison.  An estimate is 2.5 people and 0.1 Full-Time
 Equivalents.  This has a substantial impact on the handling of bug
-reports and enhancement reports; see below.  Help in terms of
-high-quality bug reports, fixes, and proposed changes is very welcome.
-
-While much of Unison activity is now at
-https://github.com/bcpierce00/unison/ additional information can be
-found at: http://www.cis.upenn.edu/~bcpierce/unison
+reports and enhancement reports; see the wiki page linked at the top.
+Help in terms of high-quality bug reports, fixes, and proposed changes
+is very welcome.  Help in answering mailinglist questions is also
+welcome.  Please do not answer questions asked in the bug tracker,
+which is contrary to bug tracker usage guidance.
+
+Unison activity is now centered on the two [Unison
+mailinglists](https://github.com/bcpierce00/unison/wiki/Mailing-Lists)
+for discussion and [Unison's github
+page](https://github.com/bcpierce00/unison/) for code, issues and a
+wiki.
+A no-longer-maintained FAQ can be found at: the [old UPenn
+site](http://www.cis.upenn.edu/~bcpierce/unison).
 
 ## Getting Unison
 
@@ -76,7 +89,7 @@ Unison.  Results from Continuous Integration builds, while performed
 for the purposes of testing, are available for use on a limited set of
 platforms.
 
-See the manual in doc/ for building instructions, or read the CI
+See the [building instructions](INSTALL.md), or read the CI
 recipes.  (Currently, this is probably less well explained than it
 should be.)
 
@@ -84,13 +97,19 @@ You may be able to find a pre-built binary for your operating system,
 version, and CPU type.  For a list of sources, See
 https://github.com/bcpierce00/unison/wiki/Downloading-Unison
 
-Generally, you should use the most recent formal release, currently
-2.51.X.  Earlier branches (e.g. 2.48) are no longer maintained, and
-bug reports are not accepted about these versions.  This is true even
-though many packaging systems (including GNU/Linux distributions)
-continue to have 2.48.  There are sometimes release candidates.  There
-is always the master branch in git, which historically has been quite
-stable.
+You should use the most recent formal release, or a newer version from
+git.  Earlier versions are no longer maintained, and bug reports are
+not accepted about these versions.  This is true even though many
+packaging systems (including GNU/Linux distributions) continue to have
+2.51 or even 2.48.  The master branch in git historically has been
+quite stable.
+
+### Version compatibility
+
+For Unison versions 2.52 and newer, see
+https://github.com/bcpierce00/unison/wiki/2.52-Migration-Guide
+
+The information below is true for Unison versions older than 2.52.
 
 Beware that Unison uses OCaml's built-in data marshalling, and that
 this facility is unstable across versions of "ocaml" (the standard
@@ -105,42 +124,6 @@ There are two mailinglists: unison-users and unison-hackers.
 Descriptions and instructions are at
 https://github.com/bcpierce00/unison/wiki/Mailing-Lists
 
-## Asking for Help and Reporting Bugs
-
-For an expanded discussion of asking for help, see
-https://github.com/bcpierce00/unison/wiki/Reporting-Bugs-and-Feature-Requests
-
-The issue tracker is for bug reports and (limited) enhancement
-requests.  Specifically, this means that questions and requests for
-help are not appropriate as issues; those should be directed to
-unison-users (or unison-hackers if the discussion requires reading the
-source code).
-
-Unison's product is the source code.  A packaging system having an old
-version is not a bug in Unison.  The CI-provided binaries exist for
-Continuous Integration and are useful for users as a side benefit.
-Therefore, the CI binaries not working on a particular operating
-system is not a Unison bug.  (In general, binaries should be provided
-by packaging systems.)
-
-A bug means that the reporter can articulate how Unison should have
-behaved and say what it did instead, either as a violation of an
-explicit specification, or an implicit specification that is likely to
-be widely viewed as valid.  Bugs should be phrased as "unison randomly
-deletes files outside of the synchronization root", summarizing the
-bad behavior (that's humor -- Unison has never even been accused of
-doing that!).
-
-An enhancement request should describe how Unison should do something
-different or additional, phrased as an imperative as something like
-"Change wire protocol to be independent of ocaml compiler version".
-Enhancement requests are appropriate if they are clearly articulated,
-and would bring benefits to users that are significant compared to
-their likely implementation effort.  Speculative or "pie in the sky"
-enhancement requests may be closed on the basis that their continued
-presence in the issue tracker has too much cognitive load compared to
-benefit, especially if the submitter doesn't intend to work on it.
-
 ## Development and Submitting Proposed Changes
 
 If you want to play with the internals, have a look at the file
diff --git a/debian/changelog b/debian/changelog
index b3a7534..45c12cf 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+unison-2.51+4.13.1 (2.53.3-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 10 May 2023 07:06:53 -0000
+
 unison-2.51+4.13.1 (2.51.5-1) unstable; urgency=medium
 
   * Compile with OCaml 4.13.1
diff --git a/doc/.depend b/doc/.depend
deleted file mode 100644
index e69de29..0000000
diff --git a/doc/Makefile b/doc/Makefile
index 41d608b..fbae46d 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -6,7 +6,7 @@ all:: unison-manual.html
 include ../src/Makefile.OCaml
 
 STRINGS = ../icons/Unison.gif
-SOURCES = unison-manual.tex changes.tex \
+SOURCES = unison-manual.tex \
           local.tex short.tex postproc$(EXEC_EXT) Makefile
 
 # ifeq ($(shell ls /usr/local/bin/hevea), /usr/local/bin/hevea)
@@ -40,17 +40,18 @@ ifeq ($(HEVEA),true)
 	latex unison-manual.tex
 	hevea unison-manual.tex
 	./postproc < unison-manual.html > temp.html
-	(TERM=vt100; export TERM; lynx -dump temp.html > unison-manual.dtxt)
+	(TERM=vt100; export TERM; lynx -display_charset=utf8 -dump temp.html > unison-manual.dtxt)
+	sed -e "/^----SNIP----/,+2 d" -e "/^Junk/,$$ d" unison-manual.dtxt > unison-manual.txt
 	./docs$(EXEC_EXT)
 endif
-	printf '$(TEXDIRECTIVES)\\textversionfalse' > texdirectives.tex
+	printf '$(TEXDIRECTIVES)\\textversionfalse\\draftfalse' > texdirectives.tex
 	latex unison-manual.tex
 	latex unison-manual.tex
 	cp unison-manual.dvi temp.dvi
 	dvips -t letter -o unison-manual.ps unison-manual.dvi
 	dvips -t letter -z -Ppdf -G0 -D600 unison-manual.dvi -o junk.ps
 	ps2pdf junk.ps unison-manual.pdf
-ifdef HEVEA
+ifeq ($(HEVEA),true)
 	hevea unison-manual.tex
 endif
 
@@ -77,12 +78,12 @@ postproc$(EXEC_EXT) : postproc.ml
 
 clean::
 	$(RM) -r \
-	   *.dtxt *.aux *.log *.out \
+	   *.dtxt *.aux *.haux *.log *.out \
 	   texdirectives.tex \
 	   junk.* *.htoc *.toc *.bak \
 	   docs docs.exe temp.dvi temp.html unison-manual.html \
 	   postproc postproc.exe postproc.ml \
 	   unison-manual.dvi unison-manual.ps unison-manual.pdf \
-	   unison-manual.info* unisonversion.tex \
+	   unison-manual.txt unison-manual.info* unisonversion.tex \
 	   contact.html faq.html faq.haux index.html
 
diff --git a/doc/changes.tex b/doc/changes.tex
deleted file mode 100644
index 477ad85..0000000
--- a/doc/changes.tex
+++ /dev/null
@@ -1,2026 +0,0 @@
-\begin{changesfromversion}{2.51.4}
-\item Restore OCaml compat to before 4.02
-\item dune/opam improvements/fixes
-\item Improve GTK UI by using GtkTreeView
-\item Add support for syncing symlinks on Windows (NTFS)
-\item  Improve ssh support on Windows (hide Windows console in GUI mode)
-\item Many bugfixes and minor improvements
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.51.3}
-\item OCaml 4.12 support
-\item fsmonitor improvements and Solaris support
-\item Color support in text UI, with a new preference, disabled by
-  NO\_COLOR.
-\item Interactive profile selection in text UI, enabled by a new
-  preference.
-\item Working files are stored in the unison directory (typically
-  ~/.unison) rather than \$HOME.
-\item Build cleanups, CI improvements, housekeeping
-\item Many bugfixes and minor improvements
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.51.2}
-\item Some nontrivial changes to profile parsing (G.raud Meyer)
-  \begin{itemize}
-  \item '=' has been considered whitespace until now: several
-    following chars are considered as only one; trailing chars are
-    discarded; any non empty sequence of char is splitting. This is non
-    standard and leads to confusion, for example -ignore== 'Name .*=*'
-    is valid when -ignore='Name .*=*' is not, and worse -ignore='Name
-    *=' is the same as -ignore='Name *'.  The parser now takes just a
-    single '=' as delimiter after the option name.  Other = characters
-    are considered as part of the value being assigned to the option.
-  \end{itemize}
-\item Numerous improvements to the text user-interface (G.raud Meyer)
-  \begin{itemize}
-  \item New key-commands that restrict the display to a set of
-    "matching" items: ones that are offering to propagate changes in a
-    particular direction, conflicts, files to be merged, etc., plus
-    several more useful key-commands.  Type "?" to Unison to see all
-    available commands.
-  \end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.48}
-\item Repository transplanted from SVN to Git and moved to GitHub (\URL{https://github.com/bcpierce00/unison}).
-\item Add a new preference, '{\tt atomic}', for specifying directories that
-  should be treated atomically: if there are changes within such a
-  directory in both replicase, the whole directory is marked as a
-  conflict instead of propagating any of the changes.  Thanks to
-  Julian Squires for submitting this patch!
-\item OSX / macOS
-  \begin{itemize}
-    \item Ported to 10.13, High Sierra, and Apple's new APFS (earlier
-    versions of Unison break because of new behavior of AppleDouble files)
-    \item Replaced Growl with OS X native notification center.
-\end{itemize}
-\item Miscellaneous:
-\begin{itemize}
-\item The OCaml compiler version is now included in the ``connection header
---- the string that's printed when connecting to a remote server --- to
-facilitate debugging version mismatch issues.  
-\item Compatible with OCaml 4.06.
-\item Added a DockerFile for the convenience of Docker users.
-\item Many small bugfixes and UI improvements.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.45}
-\item Incorporated a patch from Christopher Zimmermann to replace the
-  Uprintf module (which doesn't work with OCaml 4.02, causing Unison to crash) with equivalent
-  functionality from the standard library.
-\item Incorporated a refresh of the OSX GUI, contributed by Alan Shutko.
-\item Added a {\tt maxsizethreshold} option, which prevents the transfer of
-  files larger than the size specified (in Kb).
-\item Added a "copyonconflict" preference, to make a copy of files that would
-  otherwise be overwritten or deleted in case of conflicting changes.
-  (This makes it possible to automatically resolve conflicts in a
-   fairly safe way when synchronizing continuously, in combination
-   with the "{\tt repeat = watch}" and "{\tt prefer = newer}" preferences.
-%%%%%
-\item  File system monitoring:
-\begin{itemize}
-\item
-The file watcher now fails when unable to
-  watch a directory, rather than silently ignoring the issue.
-\item File system monitoring: more robust communication with the helper program
-  (in socket mode, the unison server will still work properly despite
-  unexpected unison client disconnections).
-\item A bytecode version of unison-fsmonitor is now produced by "make NATIVE=false"
-\item Improved search for unison-fsmonitor
-\item Detect when the helper process exits.
-\item More robust file watching helper programs for Windows and Linux.
-  They communicate with Unison through pipes (Unison redirects stdin
-  and stdout), using a race-free protocol.
-\item Retries paths with failures using an exponential backoff algorithm.
-\item  The information returned by the file watchers are used
-    independently for each replica; thus, when only one replica has
-    changes, Unison will only rescan this replica.
-\item When available, used by the graphical UIs to speed up rescanning
-    (can be disabled by setting the new {\tt watch} preference to
-\item Small fix to the way fsmonitor.py gets invoked when using the file
-  watching functionality, suggested by Josh Berdine.  Unison will now
-  look for {\tt fsmonitor.py} in the same directory where the Unison
-  executable itself lives.
-\end{itemize}
-%%%%%
-\item Minor:
-\begin{itemize}
-\item Fixed a bug in export procedure that was messing up documentation
-strings.
-\item Incorporated a patch from Ir\'anyossy Knoblauch Art\'ur to make temp file
-  names fit within 143 characters (to make eCryptFS happy).
-\item Added a string to the Conflict direction to document the reason of
-  the conflict.
-\item Log conflicts and problems in the text UI even if nothing is propagated.
-\item Use hash function from OCaml 3.x for comparing archives, even when
-  compiled with OCaml 4.x.
-\item Do not restart Unison in case of uncaught exception when the repeat
-  preference is set.  This seems safer.  And it does not work, for
-  instance, in case of lost connection.
-\item Fix Unix.readlink invalid argument error under Windows
-\item Fix a crash when the output of the {\tt diff} program is too large.
-\item Fixed Makefile for cross-compiling towards Windows (updated to MinGW-w64)
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.40.63}
-\item New preference {\tt fastercheckUNSAFE}, which can be used (with care!)
-to achieve {\em much} faster update detection when all the common files in
-the two replicas are known to be identical.  See the manual for more
-information.
-
-This feature should still be considered experimental, but it's ready for
-other people to try out.
-\item Added option {\tt clientHostName}. If specified, it will be used to as
-the client host name, overriding {\tt UNISONLOCALHOSTNAME} and the actual
-host name.
-\item OS X GUI:
-\begin{itemize}
-\item fix crash under Lion, because of problems with the toolbar, using the
-fix suggested in {\tt http://blitzbasic.com/Community/posts.php?topic=95778}.
-
-\item uimacnew09 is now the standard graphical interface on OSX
-\item A small improvement to the uimacnew09 interface from Alan Schmitt
-  and Steve Kalkwarf: when Unison is run with the -batch flag, the
-  interface will now automatically propagate changes and terminate,
-  without waiting for user interaction.
-\item Show a modal warning window if there is no archive for the hosts.  The
-user can then choose to exit or proceed (proceed is the default). The window
-is not shown if the {\tt batch} preference is true.
-\item file details panel selectable
-\end{itemize}
-\item GTK GUI:
-\begin{itemize}
-\item New version of uigtk2.ml from Matt Zagrabelny that reorganizes the
-icons in a slightly more intuitive way.
-\end{itemize}
-\item Minor fixes:
-\begin{itemize}
-\item Setting the {\tt prefer} preference to {\tt older} or {\tt newer} now
-propagates deletions when there is no conflict.
-\item Correctly quote the path when running merge commands.
-\item Add quotes to paths when calling external file watcher utility.
-\item Incorporate a patch to fsmonitor.py (the external filewatcher
-  utility) from Tomasz Zernicki to make it work better under Windows.
-\item Incorporated new version of fsmonitor.py from Christophe Gohle
-\item Fixed incompatibility with OpenSSH 5.6.
-\item Fixed fingerprint cache: do not cache file properties
-\item Some spelling corrections in documentation and comments from Stephane
-Glondu
-\item Fixed {\tt O\_APPEND} mode for open under Windows
-\item Fixed String.sub invalid argument error when an AppleDouble file does
-  not contain a finder information field
-\item Trim duplicate paths when using "-repeat watch"
-\item Unison now passes path arguments and --follow directives to
-  fsmonitor.py.  This seems to work except for one small issue with
-  how fsmonitor.py treats {\tt -follow} directives for directories that
-  don't exist (or maybe this is an issue with how it treats any kind
-  of monitoring when the thing being monitored doesn't exist?).  If we
-  create a symlink to a nonexistent directory, give Unison (hence
-  fsmonitor.py) a 'follow' directive for the symlink, start unison, and
-  {\em then} create the directory, fsmonitor.py misses the change.
-\item Lines added in profile files by unison always start at a new line
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.40.1}
-\item Added "BelowPath" patterns, that match a path as well as all paths below
-  (convenient to use with no{deletion,update,creation}partial preferences)
-\item Added a "fat" preference that makes Unison use the right options
-  when one of the replica is on a FAT filesystem.
-\item Allow "prefer/force=newer" even when not synchronizing modification
-  times.  (The reconciler will not be aware of the modification time
-  of unchanged files, so the synchronization choices of Unison can be
-  different from when "times=true", but the behavior remains sane:
-  changed files with the most recent modification time will be
-  propagated.)
-\item Minor fixes and improvements:
-\begin{itemize}
-\item Compare filenames up to decomposition in case sensitive mode when
-  one host is running MacOSX and the unicode preference is set to
-  true.
-\item Rsync: somewhat faster compressor
-\item Make Unicode the default on all architectures (it was only the
-  default when a Mac OS X or Windows machine was involved).
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.32}
-\item Major enhancement: Unicode support.
-\begin{itemize}
-\item Unison should now handle unicode filenames correctly on all platforms.
-\item This functionality is controlled by a new preference {\tt unicode}.
-\item Unicode mode is now the default when one of the hosts is under
-  Windows or MacOS.  This may make upgrades a bit more painful (the
-  archives cannot be reused), but this is a much saner default.
-\end{itemize}
-\item Partial transfer of directories.  If an error occurs while
-  transferring a directory, the part transferred so far is copied into
-  place (and the archives are updated accordingly).
-  The "maxerrors" preference controls how many transfer error Unison
-  will accept before stopping the transfer of a directory (by default,
-  only one).  This makes it possible to transfer most of a directory
-  even if there are some errors.  Currently, only the first error is
-  reported by the GUIs.
-
-  Also, allow partial transfer of a directory when there was an error deep
-  inside this directory during update detection.  At the moment, this
-  is only activated with the text and GTK UIs, which have been
-  modified so that they show that the transfer is going to be partial
-  and so that they can display all errors.
-\item Improvement to the code for resuming directory transfers:
-\begin{itemize}
-\item
-   if a file was not correctly transferred (or the source has been
-    modified since, with unchanged size), Unison performs a new
-    transfer rather than failing
-  \item spurious files are deleted (this can happen if a file is deleted
-    on the source replica before resuming the transfer; not deleting
-    the file would result in it reappearing on the target replica)
-\end{itemize}
-\item Experimental streaming protocol for transferring file contents (can
-  be disabled by setting the directive "stream" to false): file
-  contents is transferred asynchronously (without waiting for a response
-  from the destination after each chunk sent) rather than using the
-  synchronous RPC mechanism.  As a consequence:
-  \begin{itemize}
-  \item
-   Unison now transfers the contents of a single file at a time
-    (Unison used to transfer several contents simultaneously in order
-    to hide the connection latency.)
-  \item the transfer of large files uses the full available bandwidth
-    and is not slowed done due to the connection latency anymore
-  \item we get performance improvement for small files as well by
-    scheduling many files simultaneously (as scheduling a file for
-    transfer consume little resource: it does not mean allocating a
-    large buffer anymore)
-  \end{itemize}
-\item Changes to the internal implementation of the rsync algorithm:
-\begin{itemize}
-\item
-  use longer blocks for large files (the size of a block is the
-    square root of the size of the file for large files);
-  \item transmit less checksum information per block (we still have less
-    than one chance in a hundred million of transferring a file
-    incorrectly, and Unison will catch any transfer error when
-    fingerprinting the whole file)
-  \item avoid transfer overhead (which was 4 bytes per block)
-\end{itemize}
-  For a 1G file, the first optimization saves a factor 50 on the
-  amount of data transferred from the target to the source (blocks
-  are 32768 bytes rather than just 700 bytes).  The two other
-  optimizations save another factor of 2 (from 24 bytes per block
-  down to 10).
-\item Implemented an on-disk file fingerprint cache to speed-up update
-  detection after a crash: this way, Unison does not have do recompute
-  all the file fingerprints from scratch.
-  \begin{itemize}
-  \item When Unison detects that the archive case-sensitivity mode
-  does not match the current settings, it populates the fingerprint
-  cache using the archive contents.  This way, changing the
-  case-sensitivity mode should be reasonably fast.
-  \end{itemize}
-\item New preferences "noupdate=root", "nodeletion=root", "nocreation=root"
-  that prevent Unison from performing files updates, deletions or
-  creations on the given root.  Also 'partial' versions of 'noupdate',
-  'nodeletion' and 'nocreation'
-\item Limit the number of simultaneous external copy program
-  ("copymax" preference)
-\item New "links" preference.  When set to false, Unison will report an
-  error on symlinks during update detection.  (This is the default
-  when one host is running Windows but not Cygwin.)  This is better
-  than failing during propagation.
-\item Added a preference "halfduplex" to force half-duplex communication
-  with the server.  This may be useful on unreliable links (as a more
-  efficient alternative to "maxthreads = 1").
-\item Renamed preference "pretendwin" to "ignoreinodenumbers" (an alias is
-  kept for backwards compatibility).
-\item Ignore one-second differences when synchronizing modification time.
-  (Technically, this is an incompatible archive format change, but it
-   is backward compatible.  To trigger a problem, a user would have to
-   synchronize modification times on a filesystem with a two-second
-   granularity and then downgrade to a previous version of Unison,
-   which does not work well in such a case.  Thus, it does not
-   seem worthwhile to increment the archive format number, which would
-   impact all users.)
-\item Do not keep many files simultaneously opened anymore when the rsync
-  algorithm is in use.
-\item Add ``ignorearchives'' preference to ignore existing archives (to
-  avoid forcing users to delete them manually, in situations where one
-  archive has gotten   deleted or corrupted).
-\item Mac OS
-\begin{itemize}
-\item fixed rsync bug which could result in an "index out of bounds"
-  error when transferring resource forks.
-\item Fixed bug which made Unison ignore finder information and resource
-  fork when compiled to 64bit on Mac OSX.
-\item should now be 64 bit clean (the Growl framework is not up to date,
-    though)
-\item Made the bridge between Objective C and Ocaml code GC friendly
-    (it was allocating ML values and putting them in an array which
-    was not registered with the GC)
-\item use darker grey arrows (patch contributed by Eric Y. Kow)
-\end{itemize}
-\item GTK user interface
-\begin{itemize}
-\item  assistant for creating profiles
-\item profile editor
-\item pop up a summary window when the replicas are not fully
-    synchronized after transport
-\item display estimated remaining time and transfer rate on the
-  progress bar
-\item allow simultaneous selection of several items
-\item Do not reload the preference file before a new update
-  detection if it is unchanged
-\item disabled scrolling to the first unfinished item during transport.
-  It goes way too fast when lot of small files are synchronized, and it
-  makes it impossible to browse the file list during transport.
-\item take into account the "height" preference again
-\item the internal list of selected reconciler item was not always in
-    sync with what was displayed (GTK bug?); workaround implemented
-\item Do not display "Looking for change" messages during propagation
-  (when checking the targe is unchanged) but only during update detection
-\item Apply patch to fix some crashes in the OSX GUI, thanks to Onne Gorter.
-\end{itemize}
-\item Text UI
-\begin{itemize}
-\item During update detection, display status by updating a single line
-rather than generating a new line of output every so often.  Should be less
-confusing.
-\end{itemize}
-\item Windows
-\begin{itemize}
-\item Fastcheck is now the default under Windows.  People mostly use NTFS
-  nowadays and the Unicode API provides an equivalent to inode numbers
-  for this filesystem.
-\item Only use long UNC path for accessing replicas (as '..' is
-  not handled with this format of paths, but can be useful)
-\item Windows text UI: now put the console into UTF-8 output mode.  This
-  is the right thing to do when in Unicode mode, and is no worse than
-  what we had previously otherwise (the console use some esoteric
-  encoding by default).  This only works when using a Unicode font
-  instead of the default raster font.
-\item Don't get the home directory from environment variable HOME under
-  Windows (except for Cygwin binaries): we don't want the behavior of
-  Unison to depends on whether it is run from a Cygwin shell (where
-  HOME is set) or in any other way (where HOME is usually not set).
-\end{itemize}
-\item Miscellaneous fixes and improvements
-\begin{itemize}
-\item Made a server waiting on a socket more resilient to unexpected
-  lost connections from the client.
-\item Small patch to property setting code suggested by Ulrich Gernkow.
-\item Several fixes to the change transfer functions (both the internal ones
-  and external transfers using rsync).  In particular, limit the number of
-  simultaneous transfer using an rsync
-  (as the rsync algorithm can use a large amount of memory when
-   processing huge files)
-\item Keep track of which file contents are being transferred, and delay
-  the transfer of a file when another file with the same contents is
-  currently being transferred.  This way, the second transfer can be
-  skipped and replaced by a local copy.
-\item Experimental update detection optimization:
-  do not read the contents of unchanged directories
-\item When a file transfer fails, turn off fastcheck for this file on the
-  next sync.
-\item Fixed bug with case insensitive mode on a case sensitive filesystem:
-\begin{itemize}
-\item
-   if file "a/a" is created on one replica and directory "A" is
-    created on the other, the file failed to be synchronized the first
-    time Unison is run afterwards, as Unison uses the wrong path "a/a"
-    (if Unison is run again, the directories are in the archive, so
-     the right path is used);
-  \item if file "a" appears on one replica and file "A" appears on the
-    other with different contents, Unison was unable to synchronize
-    them.
-\end{itemize}
-\item Improved error reporting when the destination is updated during
-  synchronization: Unison now tells which file has been updated, and how.
-\item Limit the length of temporary file names
-\item Case sensitivity information put in the archive (in a backward
-  compatible way) and checked when the archive is loaded
-\item Got rid of the 16mb marshalling limit by marshalling to a bigarray.
-\item Resume copy of partially transferred files.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.31}
-\item Small user interface changes
-\begin{itemize}
-\item Small change to text UI "scanning..." messages, to print just
-  directories (hopefully making it clearer that individual files are
-  not necessarily being fingerprinted).
-\end{itemize}
-\item Minor fixes and improvements:
-\begin{itemize}
-\item Ignore one hour differences when deciding whether a file may have
-  been updated.  This avoids slow update detection after daylight
-  saving time changes under Windows.  This makes Unison slightly more
-  likely to miss an update, but it should be safe enough.
-\item Fix a small bug that was affecting mainly windows users.  We need to
-  commit the archives at the end of the sync even if there are no
-  updates to propagate because some files (in fact, if we've just
-  switched to DST on windows, a LOT of files) might have new modtimes
-  in the archive.  (Changed the text UI only.  It's less clear where
-  to change the GUI.)
-\item Don't delete the temp file when a transfer fails due to a
-  fingerprint mismatch (so that we can have a look and see why!)  We've also
-  added more debugging code togive more informative error messages when we
-  encounter the dreaded and longstanding "assert failed during file
-  transfer" bug
-\item Incorrect paths ("path" directive) now result in an error update
-  item rather than a fatal error.
-\item Create parent directories (with correct permissions) during
-  transport for paths which point to non-existent locations in the
-  destination replica.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.27}
-\item If Unison is interrupted during a directory transfer, it will now
-leave the partially transferred directory intact in a temporary
-location. (This maintains the invariant that new files/directories are
-transferred either completely or not at all.)  The next time Unison is run,
-it will continue filling in this temporary directory, skipping transferring
-files that it finds are already there.
-\item We've added experimental support for invoking an external file
-transfer tool for whole-file copies instead of Unison's built-in transfer
-protocol.  Three new preferences have been added:
-\begin{itemize}
-\item {\tt copyprog} is a string giving the name (and command-line
-switches, if needed) of an external program that can be used to copy large
-files efficiently.  By default, rsync is invoked, but other tools such as
-scp can be used instead by changing the value of this preference.  (Although
-this is not its primary purpose, rsync is actually a pretty fast way of
-copying files that don't already exist on the receiving host.)  For files
-that do already exist on (but that have been changed in one replica), Unison
-will always use its built-in implementation of the rsync algorithm.
-\item Added a "copyprogrest" preference, so that we can give different
-command lines for invoking the external copy utility depending on whether a
-partially transferred file already exists or not.  (Rsync doesn't seem to
-care about this, but other utilities may.)
-\item {\tt copythreshold} is an integer (-1 by default), indicating above what
-filesize (in megabytes) Unison should use the external copying utility
-specified by copyprog.  Specifying 0 will cause ALL copies to use the
-external program; a negative number will prevent any files from using it.
-(Default is -1.)
-\end{itemize}
-Thanks to Alan Schmitt for a huge amount of hacking and to an anonymous
-sponsor for suggesting and underwriting this extension.
-\item Small improvements:
-\begin{itemize}
-\item Added a new preference, {\tt dontchmod}.  By default, Unison uses the
-{\tt chmod} system call to set the permission bits of files after it has
-copied them.  But in some circumstances (and under some operating systems),
-the chmod call always fails.  Setting this preference completely prevents
-Unison from ever calling {\tt chmod}.
-\item Don't ignore files that look like backup files if the {\tt
-  backuplocation} preference is set to {\tt central}
-\item Shortened the names of several preferences.  The old names are also
-still supported, for backwards compatibility, but they do not appear in the
-documentation.
-\item Lots of little documentation tidying.  (In particular, preferences are
-separated into Basic and Advanced!  This should hopefully make Unison a
-little more approachable for new users.
-\item Unison can sometimes fail to transfer a file, giving the unhelpful
-message "Destination updated during synchronization" even though the file
-has not been changed.  This can be caused by programs that change either the
-file's contents \emph{or} the file's extended attributes without changing
-its modification time.  It's not clear what is the best fix for this -- it
-is not Unison's fault, but it makes Unison's behavior puzzling -- but at
-least Unison can be more helpful about suggesting a workaround (running once
-with {\tt fastcheck} set to false).  The failure message has been changed to
-give this advice.
-\item Further improvements to the OS X GUI (thanks to Alan Schmitt and Craig
-Federighi).
-\end{itemize}
-\item Very preliminary support for triggering Unison from an external
-  filesystem-watching utility.  The current implementation is very
-  simple, not efficient, and almost completely untested---not ready
-  for real users.  But if someone wants to help improve it (e.g.,
-  by writing a filesystem watcher for your favorite OS), please make
-  yourself known!
-
-  On the Unison side, the new behavior is very simple:
-  \begin{itemize}
-  \item use the text UI
-    \item start Unison with the command-line flag "-repeat FOO",
-      where FOO is name of a file where Unison should look
-      for notifications of changes
-    \item  when it starts up, Unison will read the whole contents
-      of this file (on both hosts), which should be a
-      newline-separated list of paths (relative to the root
-      of the synchronization) and synchronize just these paths,
-      as if it had been started with the "-path=xxx" option for
-      each one of them
-    \item when it finishes, it will sleep for a few seconds and then
-      examine the watchfile again; if anything has been added, it
-      will read the new paths, synchronize them, and go back to
-      sleep
-    \item that's it!
-  \end{itemize}
-  To use this to drive Unison "incrementally," just start it in
-  this mode and start up a tool (on each host) to watch for
-  new changes to the filesystem and append the appropriate paths
-  to the watchfile.  Hopefully such tools should not be too hard
-  to write.
-\item Bug fixes:
-\begin{itemize}
-\item Fixed a bug that was causing new files to be created with
-  permissions 0x600 instead of using a reasonable default (like
-  0x644), if the 'perms' flag was set to 0.  (Bug reported by Ben
-  Crowell.)
-\item Follow maxthreads preference when transferring directories.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.17}
-\item Major rewrite and cleanup of the whole Mac OS X graphical user
-interface by Craig Federighi.  Thanks, Craig!!!
-\item Small fix to ctime (non-)handling in update detection under windows
-  with fastcheck.
-\item Several small fixes to the GTK2 UI to make it work better under
-Windows [thanks to Karl M for these].
-\item The backup functionality has been completely rewritten.  The external
-interface has not changed, but numerous bugs, irregular behaviors, and
-cross-platform inconsistencies have been corrected.
-\item The Unison project now accepts donations via PayPal.  If you'd like to
-donate, you can find a link to the donation page on the
-\URL{http://www.cis.upenn.edu/~bcpierce/unison/lists.html}{Unison home
-  page}.
-\item Some important safety improvements:
-\begin{itemize}
-\item Added a new \verb|mountpoint| preference, which can be used to specify
-a path that must exist in both replicas at the end of update detection
-(otherwise Unison aborts).  This can be used to avoid potentially dangerous
-situations when Unison is used with removable media such as external hard
-drives and compact flash cards.
-\item The confirmation of ``big deletes'' is now controlled by a boolean preference
-  \verb|confirmbigdeletes|.  Default is true, which gives the same behavior as
-  previously.  (This functionality is at least partly superseded by the
-  \verb|mountpoint| preference, but it has been left in place in case it is
-  useful to some people.)
-  \item If Unison is asked to ``follow'' a symbolic link but there is
-  nothing at the other end of the link, it will now flag this path as an
-  error, rather than treating the symlink itself as missing or deleted.
-  This avoids a potentially dangerous situation where a followed symlink
-  points to an external filesystem that might be offline when Unison is run
-  (whereupon Unison would cheerfully delete the corresponding files in the
-  other replica!).
-\end{itemize}
-
-\item Smaller changes:
-\begin{itemize}
-\item Added \verb|forcepartial| and \verb|preferpartial| preferences, which
-behave like \verb|force| and \verb|prefer| but can be specified on a
-per-path basis. [Thanks to Alan Schmitt for this.]
-\item A bare-bones self test feature was added, which runs unison through
-  some of its paces and checks that the results are as expected.  The
-  coverage of the tests is still very limited, but the facility has already
-  been very useful in debugging the new backup functionality (especially in
-  exposing some subtle cross-platform issues).
-\item Refined debugging code so that the verbosity of individual modules
-  can be controlled separately.  Instead of just putting '-debug
-  verbose' on the command line, you can put '-debug update+', which
-  causes all the extra messages in the Update module, but not other
-  modules, to be printed.  Putting '-debug verbose' causes all modules
-  to print with maximum verbosity.
-\item Removed \verb|mergebatch| preference.  (It never seemed very useful, and
-  its semantics were confusing.)
-\item Rewrote some of the merging functionality, for better cooperation
-  with external Harmony instances.
-\item Changed the temp file prefix from \verb|.#| to \verb|.unison|.
-\item Compressed the output from the text user interface (particularly
-  when run with the \verb|-terse| flag) to make it easier to interpret the
-  results when Unison is run several times in succession from a script.
-\item Diff and merge functions now work under Windows.
-\item Changed the order of arguments to the default diff command (so that
-  the + and - annotations in diff's output are reversed).
-\item Added \verb|.mpp| files to the ``never fastcheck'' list (like
-\verb|.xls| files).
-\end{itemize}
-
-\item Many small bugfixes, including:
-\begin{itemize}
-\item Fixed a longstanding bug regarding fastcheck and daylight saving time
-  under Windows when Unison is set up to synchronize modification times.
-  (Modification times cannot be updated in the archive in this case,
-  so we have to ignore one hour differences.)
-\item Fixed a bug that would occasionally cause the archives to be left in
-  non-identical states on the two hosts after synchronization.
-\item Fixed a bug that prevented Unison from communicating correctly between
-  32- and 64-bit architectures.
-\item On windows, file creation times are no longer used as a proxy for
-  inode numbers.  (This is unfortunate, as it makes fastcheck a little less
-  safe.  But it turns out that file creation times are not reliable
-  under Windows: if a file is removed and a new file is created in its
-  place, the new one will sometimes be given the same creation date as the
-  old one!)
-\item Set read-only file to R/W on OSX before attempting to change other attributes.
-\item Fixed bug resulting in spurious "Aborted" errors during transport
-(thanks to Jerome Vouillon)
-\item Enable diff if file contents have changed in one replica, but
-only properties in the other.
-\item Removed misleading documentation for 'repeat' preference.
-\item Fixed a bug in merging code where Unison could sometimes deadlock
-  with the external merge program, if the latter produced large
-  amounts of output.
-\item Workaround for a bug compiling gtk2 user interface against current versions
-  of gtk2+ libraries.
-\item Added a better error message for "ambiguous paths".
-\item Squashed a longstanding bug that would cause file transfer to fail
-  with the message ``Failed: Error in readWrite: Is a directory.''
-\item Replaced symlinks with copies of their targets in the Growl framework in src/uimac.
-  This should make the sources easier to check out from the svn repository on WinXP
-  systems.
-\item Added a workaround (suggested by Karl M.) for the problem discussed
-  on the unison users mailing list where, on the Windows platform, the
-  server would hang when transferring files.  I conjecture that
-  the problem has to do with the RPC mechanism, which was used to
-  make a call {\em back} from the server to the client (inside the Trace.log
-  function) so that the log message would be appended to the log file on
-  the client.  The workaround is to dump these messages (about when
-  xferbycopying shortcuts are applied and whether they succeed) just to the
-  standard output of the Unison process, not to the log file.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.13.0}
-\item The features for performing backups and for invoking external merge
-programs have been completely rewritten by Stephane Lescuyer (thanks,
-Stephane!).  The user-visible functionality should not change, but the
-internals have been rationalized and there are a number of new features.
-See the manual (in particular, the description of the \verb|backupXXX|
-preferences) for details.
-\item Incorporated patches for ipv6 support, contributed by Samuel Thibault.
-(Note that, due to a bug in the released OCaml 3.08.3 compiler, this code
-will not actually work with ipv6 unless compiled with the CVS version of the
-OCaml compiler, where the bug has been fixed; however, ipv4 should continue
-to work normally.)
-\item OSX interface:
-\begin{itemize}
-\item Incorporated Ben Willmore's cool new icon for the Mac UI.
-\end{itemize}
-\item Small fixes:
-\begin{itemize}
-\item Fixed off by one error in month numbers (in printed dates) reported
-  by Bob Burger
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.12.0}
-\item New convention for release numbering: Releases will continue to be
-given numbers of the form \verb|X.Y.Z|, but,
-from now on, just the major version number (\verb|X.Y|) will be considered
-significant when checking compatibility between client and server versions.
-The third component of the version number will be used only to identify
-``patch levels'' of releases.
-
-This change goes hand in hand with a change to the procedure for making new
-releases.  Candidate releases will initially be given ``beta release''
-status when they are announced for public consumption.  Any bugs that are
-discovered will be fixed in a separate branch of the source repository
-(without changing the major version number) and new tarballs re-released as
-needed.  When this process converges, the patched beta version will be
-dubbed stable.
-\item Warning (failure in batch mode) when one path is completely emptied.
-  This prevents Unison from deleting everything on one replica when
-  the other disappear.
-\item Fix diff bug (where no difference is shown the first time the diff
-  command is given).
-\item User interface changes:
-\begin{itemize}
-\item Improved workaround for button focus problem (GTK2 UI)
-\item Put leading zeroes in date fields
-\item More robust handling of character encodings in GTK2 UI
-\item Changed format of modification time displays, from \verb|modified at hh:mm:ss on dd MMM, yyyy|
-to \verb|modified on yyyy-mm-dd hh:mm:ss|
-\item Changed time display to include seconds (so that people on FAT
-  filesystems will not be confused when Unison tries to update a file
-  time to an odd number of seconds and the filesystem truncates it to
-  an even number!)
-\item Use the diff "-u" option by default when showing differences between files
-  (the output is more readable)
-\item In text mode, pipe the diff output to a pager if the environment
-  variable PAGER is set
-\item Bug fixes and cleanups in ssh password prompting.  Now works with
-  the GTK2 UI under Linux.  (Hopefully the Mac OS X one is not broken!)
-\item Include profile name in the GTK2 window name
-\item Added bindings ',' (same as '<') and '.' (same as '>') in the GTK2 UI
-\end{itemize}
-\item Mac GUI:
-\begin{itemize}
-\item actions like < and > scroll to the next item as necessary.
-\item Restart has a menu item and keyboard shortcut (command-R).
-\item
-    Added a command-line tool for Mac OS X.  It can be installed from
-    the Unison menu.
-\item New icon.
-\item   Handle the "help" command-line argument properly.
-\item   Handle profiles given on the command line properly.
-\item  When a profile has been selected, the profile dialog is replaced by a
-    "connecting" message while the connection is being made.  This
-    gives better feedback.
-\item   Size of left and right columns is now large enough so that
-    "PropsChanged" is not cut off.
-\end{itemize}
-\item Minor changes:
-\begin{itemize}
-\item Disable multi-threading when both roots are local
-\item Improved error handling code.  In particular, make sure all files
-  are closed in case of a transient failure
-\item Under Windows, use \verb|$UNISON| for home directory as a last resort
-  (it was wrongly moved before \verb|$HOME| and \verb|$USERPROFILE| in
-  Unison 2.12.0)
-\item Reopen the logfile if its name changes (profile change)
-\item Double-check that permissions and modification times have been
-  properly set: there are some combination of OS and filesystem on
-  which setting them can fail in a silent way.
-\item Check for bad Windows filenames for pure Windows synchronization
-  also (not just cross architecture synchronization).
-  This way, filenames containing backslashes, which are not correctly
-  handled by unison, are rejected right away.
-\item Attempt to resolve issues with synchronizing modification times
-  of read-only files under Windows
-\item Ignore chmod failures when deleting files
-\item Ignore trailing dots in filenames in case insensitive mode
-\item Proper quoting of paths, files and extensions ignored using the UI
-\item The strings CURRENT1 and CURRENT2 are now correctly substitued when
-  they occur in the diff preference
-\item Improvements to syncing resource forks between Macs via a non-Mac system.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.10.2}
-\item \incompatible{} Archive format has changed.
-\item Source code availability: The Unison sources are now managed using
-  Subversion.  One nice side-effect is that anonymous checkout is now
-  possible, like this:
-\begin{verbatim}
-        svn co https://cvs.cis.upenn.edu:3690/svnroot/unison/
-\end{verbatim}
-We will also continue to export a ``developer tarball'' of the current
-(modulo one day) sources in the web export directory.  To receive commit logs
-for changes to the sources, subscribe to the \verb|unison-hackers| list
-(\ONEURL{http://www.cis.upenn.edu/~bcpierce/unison/lists.html}).
-\item Text user interface:
-\begin{itemize}
-\item Substantial reworking of the internal logic of the text UI to make it
-a bit easier to modify.
-\item The {\tt dumbtty} flag in the text UI is automatically set to true if
-the client is running on a Unix system and the {\tt EMACS} environment
-variable is set to anything other than the empty string.
-\end{itemize}
-\item Native OS X gui:
-\begin{itemize}
-\item Added a synchronize menu item with keyboard shortcut
-\item Added a merge menu item, still needs to be debugged
-\item Fixes to compile for Panther
-\item Miscellaneous improvements and bugfixes
-\end{itemize}
-\item Small changes:
-\begin{itemize}
-\item Changed the filename checking code to apply to Windows only, instead
-  of OS X as well.
-\item Finder flags now synchronized
-\item Fallback in copy.ml for filesystem that do not support \verb|O_EXCL|
-\item  Changed buffer size for local file copy (was highly inefficient with
-  synchronous writes)
-\item Ignore chmod failure when deleting a directory
-\item  Fixed assertion failure when resolving a conflict content change /
-  permission changes in favor of the content change.
-\item Workaround for transferring large files using rsync.
-\item Use buffered I/O for files (this is the only way to open files in binary
-  mode under Cygwin).
-\item On non-Cygwin Windows systems, the UNISON environment variable is now checked first to determine
-  where to look for Unison's archive and preference files, followed by \verb|HOME| and
-  \verb|USERPROFILE| in that order.  On Unix and Cygwin systems, \verb|HOME| is used.
-\item Generalized \verb|diff| preference so that it can be given either as just
-  the command name to be used for calculating diffs or else a whole command
-  line, containing the strings \verb|CURRENT1| and \verb|CURRENT2|, which will be replaced
-  by the names of the files to be diff'ed before the command is called.
-\item Recognize password prompts in some newer versions of ssh.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.9.20}
-\item \incompatible{} Archive format has changed.
-\item Major functionality changes:
-\begin{itemize}
-\item Major tidying and enhancement of 'merge' functionality.  The main
-  user-visible change is that the external merge program may either write
-  the merged output to a single new file, as before, or it may modify one or
-  both of its input files, or it may write {\em two} new files.  In the
-  latter cases, its modifications will be copied back into place on both the
-  local and the remote host, and (if the two files are now equal) the
-  archive will be updated appropriately.  More information can be found in
-  the user manual.  Thanks to Malo Denielou and Alan Schmitt for these
-  improvements.
-
-  Warning: the new merging functionality is not completely compatible with
-  old versions!  Check the manual for details.
-\item Files larger than 2Gb are now supported.
-\item Added preliminary (and still somewhat experimental) support for the
-  Apple OS X operating system.
-\begin{itemize}
-\item Resource forks should be transferred correctly.  (See the manual for
-details of how this works when synchronizing HFS with non-HFS volumes.)
-Synchronization of file type and creator information is also supported.
-\item On OSX systems, the name of the directory for storing Unison's
-archives, preference files, etc., is now determined as follows:
-\begin{itemize}
-    \item if \verb+~/.unison+ exists, use it
-     \item otherwise, use \verb|~/Library/Application Support/Unison|,
-         creating it if necessary.
-\end{itemize}
-\item A preliminary native-Cocoa user interface is under construction.  This
-still needs some work, and some users experience unpredictable crashes, so
-it is only for hackers for now.  Run make with {\tt UISTYLE=mac} to build
-this interface.
-\end{itemize}
-\end{itemize}
-
-\item Minor functionality changes:
-\begin{itemize}
-\item Added an {\tt ignorelocks} preference, which forces Unison to override left-over
-  archive locks.  (Setting this preference is dangerous!  Use it only if you
-  are positive you know what you are doing.)
-% BCP: removed later
-% \item Running with the {\tt -timers} flag set to true will now show the total time taken
-%   to check for updates on each directory.  (This can be helpful for tidying directories to improve
-%   update detection times.)
-\item Added a new preference {\tt assumeContentsAreImmutable}.  If a directory
-  matches one of the patterns set in this preference, then update detection
-  is skipped for files in this directory.  (The
-  purpose is to speed update detection for cases like Mail folders, which
-  contain lots and lots of immutable files.)  Also a preference
-  {\tt assumeContentsAreImmutableNot}, which overrides the first, similarly
-  to {\tt ignorenot}.  (Later amendment: these preferences are now called
-  {\tt immutable} and {\tt immutablenot}.)
-\item The {\tt ignorecase} flag has been changed from a boolean to a three-valued
-  preference.  The default setting, called {\tt default}, checks the operating systems
-  running on the client and server and ignores filename case if either of them is
-  OSX or Windows.  Setting ignorecase to {\tt true} or {\tt false} overrides
-  this behavior.  If you have been setting {\tt ignorecase} on the command
-  line using {\tt -ignorecase=true} or {\tt -ignorecase=false}, you will
-  need to change to {\tt -ignorecase true} or {\tt -ignorecase false}.
-\item a new preference, 'repeat', for the text user interface (only).  If 'repeat' is set to
-  a number, then, after it finishes synchronizing, Unison will wait for that many seconds and
-  then start over, continuing this way until it is killed from outside.  Setting repeat to true
-  will automatically set the batch preference to true.
-\item Excel files are now handled specially, so that the {\tt fastcheck}
-  optimization is skipped even if the {\tt fastcheck} flag is set.  (Excel
-  does some naughty things with modtimes, making this optimization
-  unreliable and leading to failures during change propagation.)
-\item The ignorecase flag has been changed from a boolean to a three-valued
-  preference.  The default setting, called 'default', checks the operating systems
-  running on the client and server and ignores filename case if either of them is
-  OSX or Windows.  Setting ignorecase to 'true' or 'false' overrides this behavior.
-\item Added a new preference, 'repeat', for the text user interface (only,
-  at the moment).  If 'repeat' is set to a number, then, after it finishes
-  synchronizing, Unison will wait for that many seconds and then start over,
-  continuing this way until it is killed from outside.  Setting repeat to
-  true will automatically set the batch preference to true.
-\item The 'rshargs' preference has been split into 'rshargs' and 'sshargs'
-  (mainly to make the documentation clearer).  In fact, 'rshargs' is no longer
-  mentioned in the documentation at all, since pretty much everybody uses
-  ssh now anyway.
-\end{itemize}
-\item Documentation
-\begin{itemize}
-\item The web pages have been completely redesigned and reorganized.
-  (Thanks to Alan Schmitt for help with this.)
-\end{itemize}
-\item User interface improvements
-\begin{itemize}
-\item Added a GTK2 user interface, capable (among other things) of displaying filenames
-  in any locale encoding.  Kudos to Stephen Tse for contributing this code!
-\item The text UI now prints a list of failed and skipped transfers at the end of
-  synchronization.
-\item Restarting update detection from the graphical UI will reload the current
-  profile (which in particular will reset the -path preference, in case
-  it has been narrowed by using the ``Recheck unsynchronized items''
-  command).
-\item Several small improvements to the text user interface, including a
-  progress display.
-\end{itemize}
-\item Bug fixes (too numerous to count, actually, but here are some):
-\begin{itemize}
-\item The {\tt maxthreads} preference works now.
-\item Fixed bug where warning message about uname returning an unrecognized
-  result was preventing connection to server.  (The warning is no longer
-  printed, and all systems where 'uname' returns anything other than 'Darwin'
-  are assumed not to be running OS X.)
-\item Fixed a problem on OS X that caused some valid file names (e.g.,
-  those including colons) to be considered invalid.
-\item Patched Path.followLink to follow links under cygwin in addition to Unix
-  (suggested by Matt Swift).
-\item Small change to the storeRootsName function, suggested by bliviero at
-  ichips.intel.com, to fix a problem in unison with the `rootalias'
-  option, which allows you to tell unison that two roots contain the same
-  files.  Rootalias was being applied after the hosts were
-  sorted, so it wouldn't work properly in all cases.
-\item Incorporated a fix by Dmitry Bely for setting utimes of read-only files
-  on Win32 systems.
-\end{itemize}
-\item Installation / portability:
-\begin{itemize}
-\item Unison now compiles with OCaml version 3.07 and later out of the box.
-\item Makefile.OCaml fixed to compile out of the box under OpenBSD.
-\item a few additional ports (e.g. OpenBSD, Zaurus/IPAQ) are now mentioned in
-  the documentation
-\item Unison can now be installed easily on OSX systems using the Fink
-  package manager
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.9.1}
-\item Added a preference {\tt maxthreads} that can be used to limit the
-number of simultaneous file transfers.
-\item Added a {\tt backupdir} preference, which controls where backup
-files are stored.
-\item Basic support added for OSX.  In particular, Unison now recognizes
-when one of the hosts being synchronized is running OSX and switches to
-a case-insensitive treatment of filenames (i.e., 'foo' and 'FOO' are
-considered to be the same file).
-  (OSX is not yet fully working,
-  however: in particular, files with resource forks will not be
-  synchronized correctly.)
-\item The same hash used to form the archive name is now also added to
-the names of the temp files created during file transfer.  The reason for
-this is that, during update detection, we are going to silently delete
-any old temp files that we find along the way, and we want to prevent
-ourselves from deleting temp files belonging to other instances of Unison
-that may be running in parallel, e.g. synchronizing with a different
-host.  Thanks to Ruslan Ermilov for this suggestion.
-\item Several small user interface improvements
-\item Documentation
-\begin{itemize}
-\item FAQ and bug reporting instructions have been split out as separate
-      HTML pages, accessible directly from the unison web page.
-\item Additions to FAQ, in particular suggestions about performance
-tuning.
-\end{itemize}
-\item Makefile
-\begin{itemize}
-\item Makefile.OCaml now sets UISTYLE=text or UISTYLE=gtk automatically,
-  depending on whether it finds lablgtk installed
-\item Unison should now compile ``out of the box'' under OSX
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.8.1}
-\item Changing profile works again under Windows
-\item File movement optimization: Unison now tries to use local copy instead of
-  transfer for moved or copied files.  It is controlled by a boolean option
-  ``xferbycopying''.
-\item Network statistics window (transfer rate, amount of data transferred).
-      [NB: not available in Windows-Cygwin version.]
-\item symlinks work under the cygwin version (which is dynamically linked).
-\item Fixed potential deadlock when synchronizing between Windows and
-Unix
-\item Small improvements:
-  \begin{itemize}
-  \item If neither the {\tt USERPROFILE} nor the {\tt HOME} environment
-    variables are set, then Unison will put its temporary commit log
-    (called {\tt DANGER.README}) into the directory named by the
-    {\tt UNISON} environment variable, if any; otherwise it will use
-    {\tt C:}.
-  \item alternative set of values for fastcheck: yes = true; no = false;
-  default = auto.
-  \item -silent implies -contactquietly
-  \end{itemize}
-\item Source code:
-  \begin{itemize}
-  \item Code reorganization and tidying.  (Started breaking up some of the
-    basic utility modules so that the non-unison-specific stuff can be
-    made available for other projects.)
-  \item several Makefile and docs changes (for release);
-  \item further comments in ``update.ml'';
-  \item connection information is not stored in global variables anymore.
-  \end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.7.78}
-\item Small bugfix to textual user interface under Unix (to avoid leaving
-  the terminal in a bad state where it would not echo inputs after Unison
-  exited).
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.7.39}
-\item Improvements to the main web page (stable and beta version docs are
-  now both accessible).
-\item User manual revised.
-\item Added some new preferences:
-\begin{itemize}
-\item ``sshcmd'' and ``rshcmd'' for specifying paths to ssh and rsh programs.
-\item ``contactquietly'' for suppressing the ``contacting server'' message
-during Unison startup (under the graphical UI).
-\end{itemize}
-\item Bug fixes:
-\begin{itemize}
-\item Fixed small bug in UI that neglected to change the displayed column
-  headers if loading a new profile caused the roots to change.
-\item Fixed a bug that would put the text UI into an infinite loop if it
-  encountered a conflict when run in batch mode.
-\item Added some code to try to fix the display of non-Ascii characters in
-  filenames on Windows systems in the GTK UI.  (This code is currently
-  untested---if you're one of the people that had reported problems with
-  display of non-ascii filenames, we'd appreciate knowing if this actually
-  fixes things.)
-\item `\verb|-prefer/-force newer|' works properly now.
-        (The bug was reported by Sebastian Urbaniak and Sean Fulton.)
-\end{itemize}
-\item User interface and Unison behavior:
-\begin{itemize}
-\item Renamed `Proceed' to `Go' in the graphical UI.
-\item Added exit status for the textual user interface.
-\item Paths that are not synchronized because of conflicts or errors during
-  update detection are now noted in the log file.
-\item \verb|[END]| messages in log now use a briefer format
-\item Changed the text UI startup sequence so that
-  {\tt ./unison -ui text} will use the default profile instead of failing.
-\item Made some improvements to the error messages.
-\item Added some debugging messages to remote.ml.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.7.7}
-\item Incorporated, once again, a multi-threaded transport sub-system.
-  It transfers several files at the same time, thereby making much
-  more effective use of available network bandwidth.  Unlike the
-  earlier attempt, this time we do not rely on the native thread
-  library of OCaml.  Instead, we implement a light-weight,
-  non-preemptive multi-thread library in OCaml directly.  This version
-  appears stable.
-
-  Some adjustments to unison are made to accommodate the multi-threaded
-  version.  These include, in particular, changes to the
-  user interface and logging, for example:
-  \begin{itemize}
-  \item Two log entries for each transferring task, one for the
-    beginning, one for the end.
-  \item Suppressed warning messages against removing temp files left
-    by a previous unison run, because warning does not work nicely
-    under multi-threading.  The temp file names are made less likely
-    to coincide with the name of a file created by the user.  They
-    take the form \\ \verb|.#<filename>.<serial>.unison.tmp|.
-    [N.b. This was later changed to \verb|.unison.<filename>.<serial>.unison.tmp|.]
-  \end{itemize}
-\item Added a new command to the GTK user interface: pressing 'f' causes
-  Unison to start a new update detection phase, using as paths {\em just}
-  those paths that have been detected as changed and not yet marked as
-  successfully completed.  Use this command to quickly restart Unison on
-  just the set of paths still needing attention after a previous run.
-\item Made the {\tt ignorecase} preference user-visible, and changed the
-  initialization code so that it can be manually set to true, even if
-  neither host is running Windows.  (This may be useful, e.g., when using
-  Unison running on a Unix system with a FAT volume mounted.)
-\item Small improvements and bug fixes:
-  \begin{itemize}
-  \item Errors in preference files now generate fatal errors rather than
-    warnings at startup time.  (I.e., you can't go on from them.)  Also,
-    we fixed a bug that was preventing these warnings from appearing in the
-    text UI, so some users who have been running (unsuspectingly) with
-    garbage in their prefs files may now get error reports.
-  \item Error reporting for preference files now provides file name and
-    line number.
-  \item More intelligible message in the case of identical change to the same
-    files: ``Nothing to do: replicas have been changed only in identical
-    ways since last sync.''
-  \item Files with prefix '.\#' excluded when scanning for preference
-    files.
-  \item Rsync instructions are send directly instead of first
-    marshaled.
-  \item Won't try forever to get the fingerprint of a continuously changing file:
-    unison will give up after certain number of retries.
-  \item Other bug fixes, including the one reported by Peter Selinger
-    (\verb|force=older preference| not working).
-  \end{itemize}
-\item Compilation:
-  \begin{itemize}
-  \item Upgraded to the new OCaml 3.04 compiler, with the LablGtk
-    1.2.3 library (patched version used for compiling under Windows).
-  \item Added the option to compile unison on the Windows platform with
-    Cygwin GNU C compiler.  This option only supports building
-    dynamically linked unison executables.
-  \end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.7.4}
-\item Fixed a silly (but debilitating) bug in the client startup sequence.
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.7.1}
-\item Added \verb|addprefsto| preference, which (when set) controls which
-preference file new preferences (e.g. new ignore patterns) are added to.
-\item Bug fix: read the initial connection header one byte at a time, so
-that we don't block if the header is shorter than expected.  (This bug
-did not affect normal operation --- it just made it hard to tell when you
-were trying to use Unison incorrectly with an old version of the server,
-since it would hang instead of giving an error message.)
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.6.59}
-\item Changed \verb|fastcheck| from a boolean to a string preference.  Its
-  legal values are \verb|yes| (for a fast check), \verb|no| (for a safe
-  check), or \verb|default| (for a fast check---which also happens to be
-  safe---when running on Unix and a safe check when on Windows).  The default
-  is \verb|default|.
-  \item Several preferences have been renamed for consistency.  All
-  preference names are now spelled out in lowercase.  For backward
-  compatibility, the old names still work, but they are not mentioned in
-  the manual any more.
-\item The temp files created by the 'diff' and 'merge' commands are now
-   named by {\em pre}pending a new prefix to the file name, rather than
-   appending a suffix.  This should avoid confusing diff/merge programs
-   that depend on the suffix  to guess the type of the file contents.
-\item We now set the keepalive option on the server socket, to make sure
-  that the server times out if the communication link is unexpectedly broken.
-\item Bug fixes:
-\begin{itemize}
-\item When updating small files, Unison now closes the destination file.
-\item File permissions are properly updated when the file is behind a
-  followed link.
-\item Several other small fixes.
-\end{itemize}
-\end{changesfromversion}
-
-
-\begin{changesfromversion}{2.6.38}
-\item Major Windows performance improvement!
-
-We've added a preference \verb|fastcheck| that makes Unison look only at
-a file's creation time and last-modified time to check whether it has
-changed.  This should result in a huge speedup when checking for updates
-in large replicas.
-
-  When this switch is set, Unison will use file creation times as
-  'pseudo inode numbers' when scanning Windows replicas for updates,
-  instead of reading the full contents of every file.  This may cause
-  Unison to miss propagating an update if the create time,
-  modification time, and length of the file are all unchanged by
-  the update (this is not easy to achieve, but it can be done).
-  However, Unison will never {\em overwrite} such an update with
-  a change from the other replica, since it
-  always does a safe check for updates just before propagating a
-  change.  Thus, it is reasonable to use this switch most of the time
-  and occasionally run Unison once with {\tt fastcheck} set to false,
-  if you are worried that Unison may have overlooked an update.
-
-  Warning: This change is has not yet been thoroughly field-tested.  If you
-  set the \verb|fastcheck| preference, pay careful attention to what
-  Unison is doing.
-
-\item New functionality: centralized backups and merging
-\begin{itemize}
-\item This version incorporates two pieces of major new functionality,
-   implemented by Sylvain Roy during a summer internship at Penn: a
-   {\em centralized backup} facility that keeps a full backup of
-   (selected files
-   in) each replica, and a {\em merging} feature that allows Unison to
-   invoke an external file-merging tool to resolve conflicting changes to
-   individual files.
-
-\item Centralized backups:
-\begin{itemize}
-  \item Unison now maintains full backups of the last-synchronized versions
-      of (some of) the files in each replica; these function both as
-      backups in the usual sense
-      and as the ``common version'' when invoking external
-      merge programs.
-  \item The backed up files are stored in a directory ~/.unison/backup on each
-      host.  (The name of this directory can be changed by setting
-      the environment variable \verb|UNISONBACKUPDIR|.)
-  \item The predicate \verb|backup| controls which files are actually
-     backed up:
-      giving the preference '\verb|backup = Path *|' causes backing up
-      of all files.
-  \item Files are added to the backup directory whenever unison updates
-      its archive.  This means that
-      \begin{itemize}
-       \item When unison reconstructs its archive from scratch (e.g.,
-           because of an upgrade, or because the archive files have
-           been manually deleted), all files will be backed up.
-       \item Otherwise, each file will be backed up the first time unison
-           propagates an update for it.
-      \end{itemize}
-  \item The preference \verb|backupversions| controls how many previous
-      versions of each file are kept.  The default is 2 (i.e., the last
-      synchronized version plus one backup).
-  \item For backward compatibility, the \verb|backups| preference is also
-      still supported, but \verb|backup| is now preferred.
-  \item It is OK to manually delete files from the backup directory (or to throw
-      away the directory itself).  Before unison uses any of these files for
-      anything important, it checks that its fingerprint matches the one
-      that it expects.
-\end{itemize}
-
-\item Merging:
-\begin{itemize}
-  \item Both user interfaces offer a new 'merge' command, invoked by pressing
-      'm' (with a changed file selected).
-  \item The actual merging is performed by an external program.
-      The preferences \verb|merge| and \verb|merge2| control how this
-      program is invoked.  If a backup exists for this file (see the
-      \verb|backup| preference), then the \verb|merge| preference is used for
-      this purpose; otherwise \verb|merge2| is used.  In both cases, the
-      value of the preference should be a string representing the command
-      that should be passed to a shell to invoke the
-      merge program.  Within this string, the special substrings
-      \verb|CURRENT1|, \verb|CURRENT2|, \verb|NEW|,  and \verb|OLD| may appear
-      at any point.  Unison will substitute these as follows before invoking
-      the command:
-        \begin{itemize}
-        \item \relax\verb|CURRENT1| is replaced by the name of the local
-        copy of the file;
-        \item \relax\verb|CURRENT2| is replaced by the name of a temporary
-        file, into which the contents of the remote copy of the file have
-        been transferred by Unison prior to performing the merge;
-        \item \relax\verb|NEW| is replaced by the name of a temporary
-        file that Unison expects to be written by the merge program when
-        it finishes, giving the desired new contents of the file; and
-        \item \relax\verb|OLD| is replaced by the name of the backed up
-        copy of the original version of the file (i.e., its state at the
-        end of the last successful run of Unison), if one exists
-        (applies only to \verb|merge|, not \verb|merge2|).
-        \end{itemize}
-      For example, on Unix systems setting the \verb|merge| preference to
-\begin{verbatim}
-   merge = diff3 -m CURRENT1 OLD CURRENT2 > NEW
-\end{verbatim}
-      will tell Unison to use the external \verb|diff3| program for merging.
-
-      A large number of external merging programs are available.  For
-      example, \verb|emacs| users may find the following convenient:
-\begin{verbatim}
-    merge2 = emacs -q --eval '(ediff-merge-files "CURRENT1" "CURRENT2"
-               nil "NEW")'
-    merge = emacs -q --eval '(ediff-merge-files-with-ancestor
-               "CURRENT1" "CURRENT2" "OLD" nil "NEW")'
-\end{verbatim}
-(These commands are displayed here on two lines to avoid running off the
-edge of the page.  In your preference file, each should be written on a
-single line.)
-
-  \item If the external program exits without leaving any file at the
-  path \verb|NEW|,
-      Unison considers the merge to have failed.  If the merge program writes
-      a file called \verb|NEW| but exits with a non-zero status code,
-      then Unison
-      considers the merge to have succeeded but to have generated conflicts.
-      In this case, it attempts to invoke an external editor so that the
-      user can resolve the conflicts.  The value of the \verb|editor|
-      preference controls what editor is invoked by Unison.  The default
-      is \verb|emacs|.
-
-  \item Please send us suggestions for other useful values of the
-       \verb|merge2| and \verb|merge| preferences -- we'd like to give several
-       examples in the manual.
-\end{itemize}
-\end{itemize}
-
-\item Smaller changes:
-\begin{itemize}
-\item When one preference file includes another, unison no longer adds the
-  suffix '\verb|.prf|' to the included file by default.  If a file with
-  precisely the given name exists in the .unison directory, it will be used;
-  otherwise Unison will
-  add \verb|.prf|, as it did before.  (This change means that included
-  preference files can be named \verb|blah.include| instead of
-  \verb|blah.prf|, so that unison will not offer them in its 'choose
-  a preference file' dialog.)
-\item For Linux systems, we now offer both a statically linked and a dynamically
-  linked executable.  The static one is larger, but will probably run on more
-  systems, since it doesn't depend on the same versions of dynamically
-  linked library modules being available.
-\item Fixed the \verb|force| and \verb|prefer| preferences, which were
-  getting the propagation direction exactly backwards.
-\item Fixed a bug in the startup code that would cause unison to crash
-  when the default profile (\verb|~/.unison/default.prf|) does not exist.
-\item Fixed a bug where, on the run when a profile is first created,
-  Unison would confusingly display the roots in reverse order in the user
-  interface.
-\end{itemize}
-
-\item For developers:
-\begin{itemize}
-\item We've added a module dependency diagram to the source distribution, in
-   \verb|src/DEPENDENCIES.ps|, to help new prospective developers with
-   navigating the code.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.6.11}
-\item \incompatible{} Archive format has changed.
-
-\item \incompatible{} The startup sequence has been completely rewritten
-and greatly simplified.  The main user-visible change is that the
-\verb|defaultpath| preference has been removed.  Its effect can be
-approximated by using multiple profiles, with \verb|include| directives
-to incorporate common settings.  All uses of \verb|defaultpath| in
-existing profiles should be changed to \verb|path|.
-
-Another change in startup behavior that will affect some users is that it
-is no longer possible to specify roots {\em both} in the profile {\em
-  and} on the command line.
-
-You can achieve a similar effect, though, by breaking your profile into
-two:
-\begin{verbatim}
-
-  default.prf =
-      root = blah
-      root = foo
-      include common
-
-  common.prf =
-      <everything else>
-\end{verbatim}
-Now do
-\begin{verbatim}
-  unison common root1 root2
-\end{verbatim}
-when you want to specify roots explicitly.
-
-\item The \verb|-prefer| and \verb|-force| options have been extended to
-allow users to specify that files with more recent modtimes should be
-propagated, writing either \verb|-prefer newer| or \verb|-force newer|.
-(For symmetry, Unison will also accept \verb|-prefer older| or
-\verb|-force older|.)  The \verb|-force older/newer| options can only be
-used when \verb|-times| is also set.
-
-The graphical user interface provides access to these facilities on a
-one-off basis via the \verb|Actions| menu.
-
-\item Names of roots can now be ``aliased'' to allow replicas to be
-relocated without changing the name of the archive file where Unison
-stores information between runs.  (This feature is for experts only.  See
-the ``Archive Files'' section of the manual for more information.)
-
-\item Graphical user-interface:
-\begin{itemize}
-\item A new command is provided in the Synchronization menu for
-  switching to a new profile without restarting Unison from scratch.
-\item The GUI also supports one-key shortcuts for commonly
-used profiles.  If a profile contains a preference of the form
-%
-'\verb|key = n|', where \verb|n| is a single digit, then pressing this
-key will cause Unison to immediately switch to this profile and begin
-synchronization again from scratch.  (Any actions that may have been
-selected for a set of changes currently being displayed will be
-discarded.)
-
-\item Each profile may include a preference '\verb|label = <string>|' giving a
-  descriptive string that described the options selected in this profile.
-  The string is listed along with the profile name in the profile selection
-  dialog, and displayed in the top-right corner of the main Unison window.
-\end{itemize}
-
-\item Minor:
-\begin{itemize}
-\item Fixed a bug that would sometimes cause the 'diff' display to order
-  the files backwards relative to the main user interface.  (Thanks
-  to Pascal Brisset for this fix.)
-\item On Unix systems, the graphical version of Unison will check the
-  \verb|DISPLAY| variable and, if it is not set, automatically fall back
-  to the textual user interface.
-\item Synchronization paths (\verb|path| preferences) are now matched
-  against the ignore preferences.  So if a path is both specified in a
-  \verb|path| preference and ignored, it will be skipped.
-\item Numerous other bugfixes and small improvements.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.6.1}
-\item The synchronization of modification times has been disabled for
-  directories.
-
-\item Preference files may now include lines of the form
-  \verb+include <name>+, which will cause \verb+name.prf+ to be read
-  at that point.
-
-\item The synchronization of permission between Windows and Unix now
-  works properly.
-
-\item A binding \verb|CYGWIN=binmode| in now added to the environment
-  so that the Cygwin port of OpenSSH works properly in a non-Cygwin
-  context.
-
-\item The \verb|servercmd| and \verb|addversionno| preferences can now
-  be used together: \verb|-addversionno| appends an appropriate
-  \verb+-NNN+ to the server command, which is found by using the value
-  of the \verb|-servercmd| preference if there is one, or else just
-  \verb|unison|.
-
-\item Both \verb|'-pref=val'| and \verb|'-pref val'| are now allowed for
-  boolean values.  (The former can be used to set a preference to false.)
-
-\item Lot of small bugs fixed.
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.5.31}
-\item The \verb|log| preference is now set to \verb|true| by default,
-  since the log file seems useful for most users.
-\item Several miscellaneous bugfixes (most involving symlinks).
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.5.25}
-\item \incompatible{} Archive format has changed (again).
-
-\item Several significant bugs introduced in 2.5.25 have been fixed.
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.5.1}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item New functionality:
-\begin{itemize}
-\item Unison now synchronizes file modtimes, user-ids, and group-ids.
-
-These new features are controlled by a set of new preferences, all of
-which are currently \verb|false| by default.
-
-\begin{itemize}
-\item When the \verb|times| preference is set to \verb|true|, file
-modification times are propaged.  (Because the representations of time
-may not have the same granularity on both replicas, Unison may not always
-be able to make the modtimes precisely equal, but it will get them as
-close as the operating systems involved allow.)
-\item When the \verb|owner| preference is set to \verb|true|, file
-ownership information is synchronized.
-\item When the \verb|group| preference is set to \verb|true|, group
-information is synchronized.
-\item When the \verb|numericIds| preference is set to \verb|true|, owner
-and group information is synchronized numerically.  By default, owner and
-group numbers are converted to names on each replica and these names are
-synchronized.  (The special user id 0 and the special group 0 are never
-mapped via user/group names even if this preference is not set.)
-\end{itemize}
-
-\item Added an integer-valued preference \verb|perms| that can be used to
-control the propagation of permission bits.  The value of this preference
-is a mask indicating which permission bits should be synchronized.  It is
-set by default to $0o1777$: all bits but the set-uid and set-gid bits are
-synchronised (synchronizing theses latter bits can be a security hazard).
-If you want to synchronize all bits, you can set the value of this
-preference to $-1$.
-
-\item Added a \verb|log| preference (default \verb|false|), which makes
-Unison keep a complete record of the changes it makes to the replicas.
-By default, this record is written to a file called \verb|unison.log| in
-the user's home directory (the value of the \verb|HOME| environment
-variable).  If you want it someplace else, set the \verb|logfile|
-preference to the full pathname you want Unison to use.
-
-\item Added an \verb|ignorenot| preference that maintains a set of patterns
-  for paths that should definitely {\em not} be ignored, whether or not
-  they match an \verb|ignore| pattern.  (That is, a path will now be ignored
-  iff it matches an ignore pattern and does not match any ignorenot patterns.)
-\end{itemize}
-
-\item User-interface improvements:
-\begin{itemize}
-\item Roots are now displayed in the user interface in the same order
-as they were given on the command line or in the preferences file.
-\item When the \verb|batch| preference is set, the graphical user interface no
-  longer waits for user confirmation when it displays a warning message: it
-  simply pops up an advisory window with a Dismiss button at the bottom and
-  keeps on going.
-\item Added a new preference for controlling how many status messages are
-  printed during update detection: \verb|statusdepth| controls the maximum
-  depth for paths on the local machine (longer paths are not displayed, nor
-  are non-directory paths).  The value should be an integer; default is 1.
-\item Removed the \verb|trace| and \verb|silent| preferences.  They did
-not seem very useful, and there were too many preferences for controlling
-output in various ways.
-\item The text UI now displays just the default command (the one that
-will be used if the user just types \verb|<return>|) instead of all
-available commands.  Typing \verb|?| will print the full list of
-possibilities.
-\item The function that finds the canonical hostname of the local host
-(which is used, for example, in calculating the name of the archive file
-used to remember which files have been synchronized) normally uses the
-\verb|gethostname| operating system call.  However, if the environment
-variable \verb|UNISONLOCALHOSTNAME| is set, its value will now be used
-instead.  This makes it easier to use Unison in situations where a
-machine's name changes frequently (e.g., because it is a laptop and gets
-moved around a lot).
-\item File owner and group are now displayed in the ``detail window'' at
-the bottom of the screen, when unison is configured to synchronize them.
-\end{itemize}
-
-\item For hackers:
-\begin{itemize}
-\item Updated to Jacques Garrigue's new version of \verb|lablgtk|, which
-  means we can throw away our local patched version.
-
-  If you're compiling the GTK version of unison from sources, you'll need
-  to update your copy of lablgtk to the developers release.
-  (Warning: installing lablgtk under Windows is currently a bit
-  challenging.)
-
-\item The TODO.txt file (in the source distribution) has been cleaned up
-and reorganized.  The list of pending tasks should be much easier to
-make sense of, for people that may want to contribute their programming
-energies.  There is also a separate file BUGS.txt for open bugs.
-\item The Tk user interface has been removed (it was not being maintained
-and no longer compiles).
-\item The \verb|debug| preference now prints quite a bit of additional
-information that should be useful for identifying sources of problems.
-\item The version number of the remote server is now checked right away
-  during the connection setup handshake, rather than later.  (Somebody
-  sent a bug report of a server crash that turned out to come from using
-  inconsistent versions: better to check this earlier and in a way that
-  can't crash either client or server.)
-\item Unison now runs correctly on 64-bit architectures (e.g. Alpha
-linux).  We will not be distributing binaries for these architectures
-ourselves (at least for a while) but if someone would like to make them
-available, we'll be glad to provide a link to them.
-\end{itemize}
-
-\item Bug fixes:
-\begin{itemize}
-\item Pattern matching (e.g. for \verb|ignore|) is now case-insensitive
-  when Unison is in case-insensitive mode (i.e., when one of the replicas
-  is on a windows machine).
-\item Some people had trouble with mysterious failures during
-  propagation of updates, where files would be falsely reported as having
-  changed during synchronization.  This should be fixed.
-\item Numerous smaller fixes.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.4.1}
-\item Added a number of 'sorting modes' for the user interface.  By
-default, conflicting changes are displayed at the top, and the rest of
-the entries are sorted in alphabetical order.  This behavior can be
-changed in the following ways:
-\begin{itemize}
-\item Setting  the \verb|sortnewfirst| preference to \verb|true| causes
-newly created files to be displayed before changed files.
-\item Setting \verb|sortbysize| causes files to be displayed in
-increasing order of size.
-\item Giving the preference \verb|sortfirst=<pattern>| (where
-\verb|<pattern>| is a path descriptor in the same format as 'ignore' and 'follow'
-patterns, causes paths matching this pattern to be displayed first.
-\item Similarly, giving the preference \verb|sortlast=<pattern>|
-causes paths matching this pattern to be displayed last.
-\end{itemize}
-The sorting preferences are described in more detail in the user manual.
-The \verb|sortnewfirst| and \verb|sortbysize| flags can also be accessed
-from the 'Sort' menu in the grpahical user interface.
-
-\item Added two new preferences that can be used to change unison's
-fundamental behavior to make it more like a mirroring tool instead of
-a synchronizer.
-\begin{itemize}
-\item Giving the preference \verb|prefer| with argument \verb|<root>|
-(by adding \verb|-prefer <root>| to the command line or \verb|prefer=<root>|)
-to your profile) means that, if there is a conflict, the contents of
-\verb|<root>|
-should be propagated to the other replica (with no questions asked).
-Non-conflicting changes are treated as usual.
-\item Giving the preference \verb|force| with argument \verb|<root>|
-will make unison resolve {\em all} differences in favor of the given
-root, even if it was the other replica that was changed.
-\end{itemize}
-These options should be used with care!  (More information is available in
-the manual.)
-
-\item Small changes:
-\begin{itemize}
-\item
-Changed default answer to 'Yes' in all two-button dialogs in the
-  graphical interface (this seems more intuitive).
-
-\item The \verb|rsync| preference has been removed (it was used to
-activate rsync compression for file transfers, but rsync compression is
-now enabled by default).
-\item  In the text user interface, the arrows indicating which direction
-changes are being
-  propagated are printed differently when the user has overridden Unison's
-  default recommendation (\verb|====>| instead of \verb|---->|).  This
-  matches the behavior of the graphical interface, which displays such
-  arrows in a different color.
-\item Carriage returns (Control-M's) are ignored at the ends of lines in
-  profiles, for Windows compatibility.
-\item All preferences are now fully documented in the user manual.
-\end{itemize}
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.3.12}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item New/improved functionality:
-\begin{itemize}
-\item  A new preference -sortbysize controls the order in which changes
-  are displayed to the user: when it is set to true, the smallest
-  changed files are displayed first.  (The default setting is false.)
-\item A new preference -sortnewfirst causes newly created files to be
-  listed before other updates in the user interface.
-\item We now allow the ssh protocol to specify a port.
-\item Incompatible change: The unison: protocol is deprecated, and we added
-  file: and socket:.  You may have to modify your profiles in the
-  .unison directory.
-  If a replica is specified without an explicit protocol, we now
-  assume it refers to a file.  (Previously "//saul/foo" meant to use
-  SSH to connect to saul, then access the foo directory.  Now it means
-  to access saul via a remote file mechanism such as samba; the old
-  effect is now achieved by writing {\tt ssh://saul/foo}.)
-\item Changed the startup sequence for the case where roots are given but
-  no profile is given on the command line.  The new behavior is to
-  use the default profile (creating it if it does not exist), and
-  temporarily override its roots.  The manual claimed that this case
-  would work by reading no profile at all, but AFAIK this was never
-  true.
-\item In all user interfaces, files with conflicts are always listed first
-\item A new preference 'sshversion' can be used to control which version
-  of ssh should be used to connect to the server.  Legal values are 1 and 2.
-  (Default is empty, which will make unison use whatever version of ssh
-  is installed as the default 'ssh' command.)
-\item The situation when the permissions of a file was updated the same on
-  both side is now handled correctly (we used to report a spurious conflict)
-
-\end{itemize}
-
-\item Improvements for the Windows version:
-\begin{itemize}
-\item The fact that filenames are treated case-insensitively under
-Windows should now be handled correctly.  The exact behavior is described
-in the cross-platform section of the manual.
-\item It should be possible to synchronize with Windows shares, e.g.,
-  //host/drive/path.
-\item Workarounds to the bug in syncing root directories in Windows.
-The most difficult thing to fix is an ocaml bug: Unix.opendir fails on
-c: in some versions of Windows.
-\end{itemize}
-
-\item Improvements to the GTK user interface (the Tk interface is no
-longer being maintained):
-\begin{itemize}
-\item The UI now displays actions differently (in blue) when they have been
-  explicitly changed by the user from Unison's default recommendation.
-\item More colorful appearance.
-\item The initial profile selection window works better.
-\item If any transfers failed, a message to this effect is displayed along with
-  'Synchronization complete' at the end of the transfer phase (in case they
-  may have scrolled off the top).
-\item Added a global progress meter, displaying the percentage of {\em total}
-  bytes that have been transferred so far.
-\end{itemize}
-
-\item Improvements to the text user interface:
-\begin{itemize}
-\item The file details will be displayed automatically when a
-  conflict is been detected.
-\item when a warning is generated (e.g. for a temporary
-  file left over from a previous run of unison) Unison will no longer
-  wait for a response if it is running in -batch mode.
-\item The UI now displays a short list of possible inputs each time it waits
-  for user interaction.
-\item The UI now quits immediately (rather than looping back and starting
-  the interaction again) if the user presses 'q' when asked whether to
-  propagate changes.
-\item Pressing 'g' in the text user interface will proceed immediately
-  with propagating updates, without asking any more questions.
-\end{itemize}
-
-\item Documentation and installation changes:
-\begin{itemize}
-\item The manual now includes a FAQ, plus sections on common problems and
-on tricks contributed by users.
-\item Both the download page and the download directory explicitly say
-what are the current stable and beta-test version numbers.
-\item The OCaml sources for the up-to-the-minute developers' version (not
-guaranteed to be stable, or even to compile, at any given time!) are now
-available from the download page.
-\item Added a subsection to the manual describing cross-platform
-  issues (case conflicts, illegal filenames)
-\end{itemize}
-
-\item Many small bug fixes and random improvements.
-
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.3.1}
-\item Several bug fixes.  The most important is a bug in the rsync
-module that would occasionally cause change propagation to fail with a
-'rename' error.
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.2}
-\item The multi-threaded transport system is now disabled by default.
-(It is not stable enough yet.)
-\item Various bug fixes.
-\item A new experimental feature:
-
-  The final component of a -path argument may now be the wildcard
-  specifier \verb|*|.  When Unison sees such a path, it expands this path on
-  the client into into the corresponding list of paths by listing the
-  contents of that directory.
-
-  Note that if you use wildcard paths from the command line, you will
-  probably need to use quotes or a backslash to prevent the * from
-  being interpreted by your shell.
-
-  If both roots are local, the contents of the first one will be used
-  for expanding wildcard paths.  (Nb: this is the first one {\em after} the
-  canonization step -- i.e., the one that is listed first in the user
-  interface -- not the one listed first on the command line or in the
-  preferences file.)
-\end{changesfromversion}
-
-\begin{changesfromversion}{2.1}
-\item The transport subsystem now includes an implementation by
-Sylvain Gommier and Norman Ramsey of Tridgell and Mackerras's
-\verb|rsync| protocol.  This protocol achieves much faster
-transfers when only a small part of a large file has been changed by
-sending just diffs.  This feature is mainly helpful for transfers over
-slow links---on fast local area networks it can actually degrade
-performance---so we have left it off by default.  Start unison with
-the \verb|-rsync| option (or put \verb|rsync=true| in your preferences
-file) to turn it on.
-
-\item ``Progress bars'' are now displayed during remote file transfers,
-showing what percentage of each file has been transferred so far.
-
-\item The version numbering scheme has changed.  New releases will now
-      be have numbers like 2.2.30, where the second component is
-      incremented on every significant public release and the third
-      component is the ``patch level.''
-
-\item Miscellaneous improvements to the GTK-based user interface.
-\item The manual  is now available in PDF format.
-
-\item We are experimenting with using a multi-threaded transport
-subsystem to transfer several files at the same time, making
-much more effective use of available network bandwidth.  This feature
-is not completely stable yet, so by default it is disabled in the
-release version of Unison.
-
-If you want to play with the multi-threaded version, you'll need to
-recompile Unison from sources (as described in the documentation),
-setting the THREADS flag in Makefile.OCaml to true.  Make sure that
-your OCaml compiler has been installed with the \verb|-with-pthreads|
-configuration option.  (You can verify this by checking whether the
-file \verb|threads/threads.cma| in the OCaml standard library
-directory contains the string \verb|-lpthread| near the end.)
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.292}
-\item Reduced memory footprint (this is especially important during
-the first run of unison, where it has to gather information about all
-the files in both repositories).
-\item Fixed a bug that would cause the socket server under NT to fail
-  after the client exits.
-\item Added a SHIFT modifier to the Ignore menu shortcut keys in GTK
-  interface (to avoid hitting them accidentally).
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.231}
-\item Tunneling over ssh is now supported in the Windows version.  See
-the installation section of the manual for detailed instructions.
-
-\item The transport subsystem now includes an implementation of the
-\verb|rsync| protocol, built by Sylvain Gommier and Norman Ramsey.
-This protocol achieves much faster transfers when only a small part of
-a large file has been changed by sending just diffs.  The rsync
-feature is off by default in the current version.  Use the
-\verb|-rsync| switch to turn it on.  (Nb.  We still have a lot of
-tuning to do: you may not notice much speedup yet.)
-
-\item We're experimenting with a multi-threaded transport subsystem,
-written by Jerome Vouillon.  The downloadable binaries are still
-single-threaded: if you want to try the multi-threaded version, you'll
-need to recompile from sources.  (Say \verb|make THREADS=true|.)
-Native thread support from the compiler is required.  Use the option
-\verb|-threads N| to select the maximal number of concurrent
-threads (default is 5).  Multi-threaded
-and single-threaded clients/servers can interoperate.
-
-\item A new GTK-based user interface is now available, thanks to
-Jacques Garrigue.  The Tk user interface still works, but we'll be
-shifting development effort to the GTK interface from now on.
-\item OCaml 3.00 is now required for compiling Unison from sources.
-The modules \verb|uitk| and \verb|myfileselect| have been changed to
-use labltk instead of camltk.  To compile the Tk interface in Windows,
-you must have ocaml-3.00 and tk8.3.  When installing tk8.3, put it in
-\verb|c:\Tcl| rather than the suggested \verb|c:\Program Files\Tcl|,
-and be sure to install the headers and libraries (which are not
-installed by default).
-
-\item Added a new \verb|-addversionno| switch, which causes unison to
-use \verb|unison-<currentversionnumber>| instead of just \verb|unison|
-as the remote server command.  This allows multiple versions of unison
-to coexist conveniently on the same server: whichever version is run
-on the client, the same version will be selected on the server.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.219}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item This version fixes several annoying bugs, including:
-\begin{itemize}
-\item Some cases where propagation of file permissions was not
-working.
-\item umask is now ignored when creating directories
-\item directories are create writable, so that a read-only directory and
-    its contents can be propagated.
-\item Handling of warnings generated by the server.
-\item Synchronizing a path whose parent is not a directory on both sides is
-now flagged as erroneous.
-\item Fixed some bugs related to symnbolic links and nonexistent roots.
-\begin{itemize}
-\item
-   When a change (deletion or new contents) is propagated onto a
-     'follow'ed symlink, the file pointed to by the link is now changed.
-     (We used to change the link itself, which doesn't fit our assertion
-     that 'follow' means the link is completely invisible)
-   \item When one root did not exist, propagating the other root on top of it
-     used to fail, because unison could not calculate the working directory
-     into which to write changes.  This should be fixed.
-\end{itemize}
-\end{itemize}
-
-\item A human-readable timestamp has been added to Unison's archive files.
-
-\item The semantics of Path and Name regular expressions now
-correspond better.
-
-\item Some minor improvements to the text UI (e.g. a command for going
-back to previous items)
-
-\item The organization of the export directory has changed --- should
-be easier to find / download things now.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.200}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item This version has not been tested extensively on Windows.
-
-\item Major internal changes designed to make unison safer to run
-at the same time as the replicas are being changed by the user.
-
-\item Internal performance improvements.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.190}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item A number of internal functions have been changed to reduce the
-amount of memory allocation, especially during the first
-synchronization.  This should help power users with very big replicas.
-
-\item Reimplementation of low-level remote procedure call stuff, in
-preparation for adding rsync-like smart file transfer in a later
-release.
-
-\item Miscellaneous bug fixes.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.180}
-\item \incompatible{} Archive format has changed.  Make sure you
-synchronize your replicas before upgrading, to avoid spurious
-conflicts.  The first sync after upgrading will be slow.
-
-\item Fixed some small bugs in the interpretation of ignore patterns.
-
-\item Fixed some problems that were preventing the Windows version
-from working correctly when click-started.
-
-\item Fixes to treatment of file permissions under Windows, which were
-causing spurious reports of different permissions when synchronizing
-between windows and unix systems.
-
-\item Fixed one more non-tail-recursive list processing function,
-which was causing stack overflows when synchronizing very large
-replicas.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.169}
-\item The text user interface now provides commands for ignoring
-  files.
-\item We found and fixed some {\em more} non-tail-recursive list
-  processing functions.  Some power users have reported success with
-  very large replicas.
-\item \incompatible
-Files ending in \verb|.tmp| are no longer ignored automatically.  If you want
-to ignore such files, put an appropriate ignore pattern in your profile.
-
-\item \incompatible{} The syntax of {\tt ignore} and {\tt follow}
-patterns has changed. Instead of putting a line of the form
-\begin{verbatim}
-                 ignore = <regexp>
-\end{verbatim}
-  in your profile ({\tt .unison/default.prf}), you should put:
-\begin{verbatim}
-                 ignore = Regex <regexp>
-\end{verbatim}
-Moreover, two other styles of pattern are also recognized:
-\begin{verbatim}
-                 ignore = Name <name>
-\end{verbatim}
-matches any path in which one component matches \verb|<name>|, while
-\begin{verbatim}
-                 ignore = Path <path>
-\end{verbatim}
-matches exactly the path \verb|<path>|.
-
-Standard ``globbing'' conventions can be used in \verb|<name>| and
-\verb|<path>|:
-\begin{itemize}
-\item a \verb|?| matches any single character except \verb|/|
-\item a \verb|*| matches any sequence of characters not including \verb|/|
-\item \verb|[xyz]| matches any character from the set $\{{\tt x},
-  {\tt y}, {\tt z} \}$
-\item \verb|{a,bb,ccc}| matches any one of \verb|a|, \verb|bb|, or
-  \verb|ccc|.
-\end{itemize}
-
-See the user manual for some examples.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.146}
-\item Some users were reporting stack overflows when synchronizing
-  huge directories.  We found and fixed some non-tail-recursive list
-  processing functions, which we hope will solve the problem.  Please
-  give it a try and let us know.
-\item Major additions to the documentation.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.142}
-\item Major internal tidying and many small bugfixes.
-\item Major additions to the user manual.
-\item Unison can now be started with no arguments -- it will prompt
-automatically for the name of a profile file containing the roots to
-be synchronized.  This makes it possible to start the graphical UI
-from a desktop icon.
-\item Fixed a small bug where the text UI on NT was raising a 'no such
-  signal' exception.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.139}
-\item The precompiled windows binary in the last release was compiled
-with an old OCaml compiler, causing propagation of permissions not to
-work (and perhaps leading to some other strange behaviors we've heard
-reports about).  This has been corrected.  If you're using precompiled
-binaries on Windows, please upgrade.
-\item Added a \verb|-debug| command line flag, which controls debugging
-of various modules.  Say \verb|-debug XXX| to enable debug tracing for
-module \verb|XXX|, or \verb|-debug all| to turn on absolutely everything.
-\item Fixed a small bug where the text UI on NT was raising a 'no such signal'
-exception.
-\end{changesfromversion}
-
-\begin{changesfromversion}{1.111}
-\item \incompatible{} The names and formats of the preference files in
-the .unison directory have changed.  In particular:
-\begin{itemize}
-\item the file ``prefs'' should be renamed to default.prf
-\item the contents of the file ``ignore'' should be merged into
-  default.prf.  Each line of the form \verb|REGEXP| in ignore should
-  become a line of the form \verb|ignore = REGEXP| in default.prf.
-\end{itemize}
-\item Unison now handles permission bits and  symbolic links.  See the
-manual for details.
-
-\item You can now have different preference files in your .unison
-directory.  If you start unison like this
-\begin{verbatim}
-             unison profilename
-\end{verbatim}
-(i.e. with just one ``anonymous'' command-line argument), then the
-file \verb|~/.unison/profilename.prf| will be loaded instead of
-\verb|default.prf|.
-
-\item Some improvements to terminal handling in the text user interface
-
-\item Added a switch -killServer that terminates the remote server process
-when the unison client is shutting down, even when using sockets for
-communication.  (By default, a remote server created using ssh/rsh is
-terminated automatically, while a socket server is left running.)
-\item When started in 'socket server' mode, unison prints 'server started' on
-  stderr when it is ready to accept connections.
-  (This may be useful for scripts that want to tell when a socket-mode server
-  has finished initialization.)
-\item We now make a nightly mirror of our current internal development
-  tree, in case anyone wants an up-to-the-minute version to hack
-  around with.
-\item Added a file CONTRIB with some suggestions for how to help us
-make Unison better.
-\end{changesfromversion}
diff --git a/doc/docs.ml b/doc/docs.ml
index ab084ae..7e6f376 100644
--- a/doc/docs.ml
+++ b/doc/docs.ml
@@ -55,9 +55,6 @@ let prmanual() =
   close_in ch;
   fprintf ml "    [];;\n\n" in
 
-(* FIX: this should be derived automatically from projectInfo.ml *)
-let myName = "unison" in
-
 (* Docs *)
 prmanual ();
 
diff --git a/doc/hevea.sty b/doc/hevea.sty
index 51a17aa..3aab95b 100644
--- a/doc/hevea.sty
+++ b/doc/hevea.sty
@@ -1,11 +1,11 @@
-% htmlgen Verion 0.0 : html.sty
+% htmlgen Version 0.0 : html.sty
 % This is a very basic style file for latex document to be processed
 % with htmlgen. It contains definitions of LaTeX commands which are
 % processed in a special way by the translator.
 %  Mostly :
 %     - environment latexonly, not processed by htmlgen, processed by latex.
 %     - environment htmlonly , the reverse
-%      - environemnt htmlraw,  to include raw HTML in hevea output.
+%      - environment htmlraw,  to include raw HTML in hevea output.
 %
 \makeatletter%
 
@@ -148,13 +148,13 @@
 % Basic approach:
 % to comment something out, scoop up  every line in verbatim mode
 % as macro argument, then throw it away.
-% For inclusions, both the opening and closing comands
+% For inclusions, both the opening and closing commands
 % are defined as noop
 %
 % Changed \next to \html@next to prevent clashes with other sty files
 % (mike@emn.fr)
 % Changed \html@next to \htmlnext so the \makeatletter and
-% \makeatother commands could be removed (they were cuasing other
+% \makeatother commands could be removed (they were causing other
 % style files - changebar.sty - to crash) (nikos@cbl.leeds.ac.uk)
 
 
diff --git a/doc/local.tex b/doc/local.tex
index c8941fc..c773436 100644
--- a/doc/local.tex
+++ b/doc/local.tex
@@ -84,7 +84,7 @@
   \ifhevea
     \SNIP{#1}{#3}%
     \iftextversion\else \@print{<hr>}\fi%
-    \section*{\label{#2}#1}%
+    \section*{\aname{#2}#1}%
   \else
     \newpage
     \section{\label{#2}#1}%
@@ -94,7 +94,7 @@
 
 \newcommand{\SUBSECTION}[2]{%
   \ifhevea
-    \subsection*{\label{#2}#1}%
+    \subsection*{\aname{#2}#1}%
   \else
     \subsection{\label{#2}#1}%
     \addtocontents{htoc}{\hspace{10em}\bullet\string\urlref{#2}{#1}\\}
@@ -103,7 +103,7 @@
 
 \newcommand{\SUBSUBSECTION}[2]{%
   \ifhevea
-    \subsubsection*{\label{#2}#1}%
+    \subsubsection*{\aname{#2}#1}%
   \else
     \subsubsection{\label{#2}#1}%
     \addtocontents{htoc}{\hspace{18em}\string\urlref{#2}{#1}\\}
diff --git a/doc/unison-manual.tex b/doc/unison-manual.tex
old mode 100755
new mode 100644
index 9c869c9..61eeaab
--- a/doc/unison-manual.tex
+++ b/doc/unison-manual.tex
@@ -4,6 +4,7 @@
 \usepackage{moreverb}
 % \usepackage{hyperref}
 \usepackage{hevea}
+\ifhevea\@def@charset{UTF-8}\fi
 
 \input{local}
 \fulltrue
@@ -230,10 +231,9 @@ Either click-start it, or type \showtt{unison -version} at the command
 line.
 
 Unison can be used in three different modes: with different directories on a
-single machine, with a remote machine over a direct socket connection, or
+single machine, with a local or a remote machine over a direct socket connection, or
 with a remote machine using {\tt ssh} for authentication and secure
-transfer.  If you intend to use the last option, you may need to install
-{\tt ssh}; see \sectionref{ssh}{Installing Ssh}.
+transfer.
 
 \SUBSECTION{Running Unison}{afterinstall}
 
@@ -256,6 +256,84 @@ you roll back to the previous version of Unison, you will find the old
 archives intact), which means that any differences between the replicas will
 show up as conflicts that need to be resolved manually.
 
+As of version 2.52, Unison has a degree of backward and forward
+compatibility.  This means three things.  First, it is possible for local
+and remote machines to run a different version of Unison.  Second, it is
+possible for local and remote machines to run a version (same or different)
+of Unison built with a different version of OCaml compiler (this has been
+problematic historically).  Lastly, it is possible to upgrade Unison on
+the local machine (compiled with any OCaml version) and keep the existing
+archive.
+
+If version interoperability requirements are followed then Unison 2.52 and
+newer can upgrade the archive created by earlier Unison versions.  To avoid
+rebuilding archive files when upgrading from a version older than 2.52, you
+must install version 2.52 or newer built with the same OCaml version as your
+previous version of Unison, and then run it at least once on each root.  Doing
+so will upgrade the archive file.
+
+After upgrading the archive, you are free to swap the Unison 2.52 or newer
+executable to one compiled with a different version of OCaml.
+The archive file is no longer dependent on the compiler version.
+
+\SUBSUBSECTION{Version interoperability}{interoperability}
+
+To ensure interoperability with different Unison versions on local and
+remote machines, and to upgrade from an earlier version {\em without
+rebuilding the archive files}, you have to remember these guidelines.
+Upgrading from an incompatible version, while possible and normal, will
+require fully scanning both roots, which can be time-consuming with big
+replicas.
+
+{\bf Unison 2.52 and newer} are compatible with:
+\begin{itemize}
+\item {\em Unison 2.52 or newer} (for as long as backwards compatibility
+is maintained in the newer versions). You do not have to pay any attention
+to OCaml compiler versions.
+\item {\em Unison 2.51} if both versions are compiled with same OCaml
+compiler version (you can see which compiler version was used by running
+{\tt unison -version}).
+\item {\em Unison 2.48} if both versions are compiled with same OCaml
+compiler version.  See special notes below.
+\end{itemize}
+
+\vspace{1em}
+\noindent {\bf Interoperability matrix} for quick reference:
+
+\vspace{1em}
+\begin{tabular}{r||c|c|c}
+  Client versions & \multicolumn{3}{c}{Server versions} \\
+  & 2.52 or newer & 2.51  & 2.48 \\
+  \hline \hline
+  2.52 or newer & full interop & same OCaml version & same OCaml version \\
+  \hline
+  2.51 & same OCaml version & full interop & no interop \\
+  \hline
+  2.48 & same OCaml version* & no interop & full interop \\
+\end{tabular}
+\vspace{2em}
+
+\noindent {\it Special notes for Unison 2.48:}
+\begin{itemize}
+\item Unison 2.48 does not show which OCaml compiler was used to compile it.
+If you do not have the option of re-compiling the 2.48 version, you have
+two alternatives.  First (and most likely to succeed), see what is the version
+of the OCaml compiler in the same package repository where you installed
+Unison 2.48 from, then use Unison 2.52 compiled with that version.
+Second, you can just try Unison 2.52 executables compiled with different
+OCaml versions and see which one works with your copy of Unison 2.48.
+\item When running Unison 2.48 on the client machine with Unison 2.52 or
+newer on the server machine, you have to do some additional configuration.
+The Unison executable name on the server must start with \verb|unison-2.48|
+(just \verb|unison-2.48| is ok, as is \verb|unison-2.48.exe|, but also
+\verb|unison-2.48+ocaml-4.05|).  If using TCP socket connection to the
+server then you're all set!  If using {\tt ssh} then you have to add one
+of the following options to your profile or as a command-line argument
+on the client machine: \verb|-addversionno|; see
+\sectionref{remote}{Remote Usage}, or \verb|-servercmd|; see
+\sectionref{rshmeth}{Remote Shell Method}.
+\end{itemize}
+
 
 \SUBSECTION{Building Unison from Scratch}{building}
 
@@ -267,106 +345,7 @@ ported and on which the \verb|Unix| module is fully implemented.  It has
 been tested on many flavors of Windows (98, NT, 2000, XP) and Unix (OS X,
 Solaris, Linux, FreeBSD), and on both 32- and 64-bit architectures.
 
-
-\SUBSUBSECTION{Unix}{build-unix}
-
-Unison can be built with or without a graphical user interface (GUI). The
-build system will decide automatically depending on the libraries installed
-on your system, but you can also type {\tt make UISTYLE=text} to build
-Unison without GUI.
-
-You'll need the Objective Caml compiler,
-available from \ONEURL{http://caml.inria.fr}.  OCaml is available from most
-package managers
-Building and installing OCaml
-on Unix systems is very straightforward; just follow the instructions in the
-distribution.  You'll probably want to build the native-code compiler in
-addition to the bytecode compiler, as Unison runs much faster when compiled
-to native code, but this is not absolutely necessary.
-%
-(Quick start: on many systems, the following sequence of commands will
-get you a working and installed compiler: first do {\tt make world opt},
-then {\tt su} to root and do {\tt make install}.)
-
-You'll also need the GNU {\tt make} utility, which is standard on most Unix
-systems.  Unison's build system is not parallelizable, so don't use flags
-that cause it to start processes in parallel (e.g. -j).
-
-Once you've got OCaml installed, grab a copy of the Unison sources, unzip
-and untar them, change to the new \showtt{unison} directory, and type ``{\tt
-  make UISTYLE=text}''.  The result should be an executable file called
-\showtt{unison}.  Type \showtt{./unison} to make sure the program is
-executable.  You should get back a usage message.
-
-If you want to build the graphical user interface, you will need to install
-some additional things:
-\begin{itemize}
-\item The Gtk2 development libraries (package {\tt libgtk2.0-dev} on debian
-based systems).
-\item OCaml bindings for Gtk2. Install them from your software repositories
-(package {\tt liblablgtk2-ocaml} on debian based systems). Also available
-from \ONEURL{http://wwwfun.kurims.kyoto-u.ac.jp/soft/olabl/lablgtk.html}.
-\item Pango, a text rendering library and a part of Gtk2. On some systems
-(e.g. Ubuntu) the bindings between Pango and OCaml need to be installed
-explicitly (package {\tt liblablgtk-extras-ocaml-dev} on Ubuntu).
-\end{itemize}
-Type {\tt make src} to build Unison. If Gtk2 is available on the system,
-Unison with a GUI will be built automatically.
-
-Put the \verb|unison| executable somewhere in your search path, either by
-adding the Unison directory to your PATH variable or by copying the
-executable to some standard directory where executables are stored.  Or just
-type {\tt make install} to install Unison to {\tt \$HOME/bin/unison}.
-
-\SUBSUBSECTION{Mac OS X}{build-osx}
-
-To build the text-only user interface, follow the instructions above for
-building on Unix systems.  You should do this first, even if you are also
-planning on building the GUI, just to make sure it works.
-
-To build the basic GUI version, you'll first need to download and install
-the XCode developer tools from Apple.  Once this is done, just type {\tt
-  make} in the {\tt src} directory, and if things go well you
-should get an application that you can move from {\tt
-  uimac/build/Default/Unison.app} to wherever you want it.
-
-\SUBSUBSECTION{Windows}{build-win}
-
-Although the binary distribution should work on any version of Windows,
-some people may want to build Unison from scratch on those systems too.
-
-\paragraph{Bytecode version:} The simpler but slower compilation option
-to build a Unison executable is to build a bytecode version.  You need
-first install Windows version of the OCaml compiler (version 3.07 or
-later, available from \ONEURL{http://caml.inria.fr}).  Then grab a copy
-of Unison sources and type
-\begin{verbatim}
-       make NATIVE=false
-\end{verbatim}
-to compile the bytecode.  The result should be an executable file called
-\verb|unison.exe|.
-
-\paragraph{Native version:} Building a more efficient, native version of
-Unison on Windows requires a little more work.  See the file {\tt
-  INSTALL.win32} in the source code distribution.
-
-
-\SUBSUBSECTION{Installation Options}{build-opts}
-
-The \verb|Makefile| in the distribution includes several switches that
-can be used to control how Unison is built.  Here are the most useful
-ones:
-\begin{itemize}
-\item Building with \verb|NATIVE=true| uses the native-code OCaml
-compiler, yielding an executable that will run quite a bit faster. We use
-this for building distribution versions.
-\item Building with \verb|make DEBUGGING=true| generates debugging
-symbols.
-\item Building with \verb|make STATIC=true| generates a (mostly)
-statically linked executable.  We use this for building distribution
-versions, for portability.
-\end{itemize}
-%\finish{Any other important ones?}
+Building instructions are included with the source code.
 
 
 \SECTION{Tutorial}{tutorial}{tutorial}
@@ -536,6 +515,13 @@ when the file has been dealt with.
 Next, we'll get Unison set up to synchronize replicas on two different
 machines.
 
+NB: Unison has not been designed to run with elevated privileges
+(e.g. setuid), and it has not been audited for that environment.
+Therefore Unison should be run with the userid of the owner of the
+files to be synchronized, and should never be run setuid or similar.
+(Problems encountered when running setuid etc. must be reproduced
+without setuid before being reported as bugs.)
+
 Follow the instructions in the Installation section to download or
 build an executable version of Unison on the server machine, and
 install it somewhere on your search path.  (It doesn't matter whether
@@ -556,11 +542,15 @@ communicating between the client and the server:
   command line, using a facility such as \verb|ssh|.
   This method is more convenient (since there is no need to manually
   start a ``unison server'' process on the server) and also more
-  secure (especially if you use \verb|ssh|).
+  secure, assuming you are using \verb|ssh|).
 
-\item {\em Socket method}: This method requires only that you can get
-  TCP packets from the client to the server and back.  A draconian
-  firewall can prevent this, but otherwise it should work anywhere.
+\item {\em TCP socket method}: This method requires only that you can
+  get TCP packets from the client to the server and back.  It is
+  insecure and should not be used.
+
+\item {\em Unix socket method}: This method only works within a
+  single machine.  It is similar to the TCP sockets method, but it is
+  possible to configure it securely.
 \end{itemize}
 
 Decide which of these you want to try, and continue with
@@ -570,18 +560,13 @@ Decide which of these you want to try, and continue with
 
 \SUBSECTION{Remote Shell Method}{rshmeth}
 
-The standard remote shell facility on Unix systems is \verb|ssh|, which provides the
-same functionality as the older \verb|rsh| but much better security.  Ssh is available from
-\ONEURL{http://www.openssh.org}.  See section~\ref{ssh-win}
-for installation instructions for the Windows version.
+The standard remote shell facility on Unix systems is \verb|ssh|.
 
 Running
 \verb|ssh| requires some coordination between the client and server
 machines to establish that the client is allowed to invoke commands on
 the server; please refer to the \verb|ssh| documentation
-for information on how to set this up.  The examples in this section
-use \verb|ssh|, but you can substitute \verb|rsh| for \verb|ssh| if
-you wish.
+for information on how to set this up.
 
 First, test that we can invoke Unison on the server from the client.
 Typing
@@ -638,18 +623,37 @@ of the path:
   on the server by using the command-line option \showtt{-servercmd
     /full/path/name/of/unison} or adding
   \showtt{servercmd=/full/path/name/of/unison} to your profile (see
-  \sectionref{profile}{Profiles}).  Similarly, you can specify a
+  \sectionref{profile}{Profiles}).  Similarly, you can specify an
   explicit path for the \verb|ssh| program using the \showtt{-sshcmd}
   option.
   Extra arguments can be passed to \verb|ssh| by setting the
   \verb|-sshargs| preference.
+
+\item By leveraging \showtt{-sshcmd} and \showtt{-sshargs}, you can
+  effectively use any remote shell program, not just \verb|ssh|; just
+  remember that the roots are still specified with \verb|ssh| as the
+  protocol, that is, they have to start with \showtt{ssh://}.
 \end{itemize}
 
 
 \SUBSECTION{Socket Method}{socketmeth}
 
+To run Unison over a socket connection, you must start a Unison
+daemon process on the server.  This process runs continuously,
+waiting for connections over a given socket from client machines
+running Unison and processing their requests in turn.
+
+Since the socket method is not used by many people, its functionality is
+rather limited.  For example, the server can only deal with one client at a
+time.
+
+Note that the Unison daemon process is always started with a command-line
+argument; not from a profile.
+
+\SUBSUBSECTION{TCP Sockets}{socket-tcp}
+
 \begin{quote}
-  {\bf\ifhevea\red\fi Warning:} The socket method is
+  {\bf\ifhevea\red\fi Warning:} The TCP socket method is
   insecure: not only are the texts of your changes transmitted over
   the network in unprotected form, it is also possible for anyone in
   the world to connect to the server process and read out the contents
@@ -660,31 +664,27 @@ of the path:
   else should use the \verb|ssh| method.
 \end{quote}
 
-To run Unison over a socket connection, you must start a Unison
-daemon process on the server.  This process runs continuously,
-waiting for connections over a given socket from client machines
-running Unison and processing their requests in turn.
-
-Note that socket mode cannot be started from a profile. It should be started as a command-line argument only.
 
-To start the daemon, type
+To start the daemon for connections over a TCP socket, type
 \begin{verbatim}
        unison -socket NNNN
 \end{verbatim}
-on the server machine, where {\tt NNNN} is the socket number that the
+on the server machine, where {\tt NNNN} is the TCP port number that the
 daemon should listen on for connections from clients.  ({\tt NNNN} can
 be any large number that is not being used by some other program; if
 \texttt{NNNN} is already in use, Unison will exit with an error
-message.)  Note that paths specified by the client will be interpreted
-relative to the directory in which you start the server process; this
-behavior is different from the ssh case, where the path is relative to
-your home directory on the server.
+message.)
 
 Create a test directory {\tt a.tmp} in your home directory on the
 client machine.  Now type:
 \begin{alltt}
        unison a.tmp socket://\NT{remotehostname}:NNNN/a.tmp
 \end{alltt}
+Note that paths specified by the client will be interpreted relative
+to the directory in which you start the server process; this behavior
+is different from the ssh case, where the path is relative to your home
+directory on the server.
+%
 The result should be that the entire directory {\tt a.tmp} is
 propagated from the client to the server (\texttt{a.tmp} will be
 created on the server in the directory that the server was started
@@ -694,10 +694,40 @@ After finishing the first synchronization, change a few files and try
 synchronizing again.  You should see similar results as in the local
 case.
 
-Since the socket method is not used by many people, its functionality is
-rather limited.  For example, the server can only deal with one client at a
-time.
+By default Unison will listen for incoming connections on all interfaces.
+If you want to limit this to certain interfaces or addresses then you
+can use the {\tt -listen} command-line argument, specifying a host name
+or an IP address to listen on. {\tt -listen} can be given multiple
+times to listen on several addresses.
 
+\SUBSUBSECTION{Unix Domain Sockets}{socket-unix}
+
+To start the daemon for connections over a Unix domain socket, type
+\begin{verbatim}
+       unison -socket PPPP
+\end{verbatim}
+where {\tt PPPP} is the path to a Unix socket that the daemon should
+open for connections from clients.  ({\tt PPPP} can be any absolute or
+relative path the server process has access to but it must not exist
+yet; the socket is created at that path when the daemon process is
+started.)  You are responsible for securing access to the socket path.
+For example, this can be done by controlling the permissions of
+socket's parent directory, or ensuring a restrictive {\tt umask} value
+when starting Unison.
+
+Clients can connect to a server over a Unix domain socket by specifying
+the absolute or relative path to the socket, instead of a server address
+and port number:
+\begin{alltt}
+       unison a.tmp socket://\{\NT{path/to/unix/socket}\}/a.tmp
+\end{alltt}
+(socket path is enclosed in curly braces).
+
+Note that Unix domain sockets are local sockets (they exist in the
+filesystem namespace).
+One could use Unixs socket remotely, by forwarding access to the
+socket by other means, for example by using {\tt spiped} secure pipe
+daemon.
 
 \SUBSECTION{Using Unison for All Your Files}{usingit}
 
@@ -792,7 +822,7 @@ lists, to receive announcements of new versions.  See
 
 To understand how Unison works, it is necessary to discuss a few
 straightforward concepts.
-%
+
 These concepts are developed more rigorously and at more length in a number
 of papers, available at \ONEURL{http://www.cis.upenn.edu/\home{bcpierce}/papers}.
 But the informal presentation here should be enough for most users.
@@ -815,21 +845,29 @@ started, while
 \noindent
 specifies a root relative to the top of the local filesystem,
 independent of where Unison is running.  Remote roots can begin with
-\verb|ssh://|,
-\verb|rsh://|
-to indicate that the remote server should be started with rsh or ssh:
+\verb|ssh://|
+to indicate that the remote server should be started with ssh:
 \begin{alltt}
       ssh://\NT{remotehost}//\NT{absolute/path/of/root}
-      rsh://\NT{user}@\NT{remotehost}/\NT{relative/path/of/root}
+      ssh://\NT{user}@\NT{remotehost}/\NT{relative/path/of/root}
 \end{alltt}
 If the remote server is already running (in the socket mode), then the syntax
 \begin{alltt}
       socket://\NT{remotehost}:\NT{portnum}//\NT{absolute/path/of/root}
       socket://\NT{remotehost}:\NT{portnum}/\NT{relative/path/of/root}
+      socket://[\NT{IPv6literal}]:\NT{portnum}/\NT{path}
 \end{alltt}
 \noindent
 is used to specify the hostname and the port that the client Unison should
 use to contact it.
+Syntax
+\begin{alltt}
+      socket://\{\NT{path/of/socket}\}//\NT{absolute/path/of/root}
+      socket://\{\NT{path/of/socket}\}/\NT{relative/path/of/root}
+\end{alltt}
+\noindent
+is used to specify the Unix domain socket the client Unison should use to
+contact the server.
 
 The syntax for roots is based on that of URIs (described in RFC 2396).
 The full grammar is:
@@ -840,11 +878,14 @@ The full grammar is:
   \NT{protocol} ::= file
             |  socket
             |  ssh
-            |  rsh
 
-  \NT{user} ::= [-_a-zA-Z0-9]+
+  \NT{user} ::= [-\_a-zA-Z0-9]+
+
+  \NT{host} ::= [-\_a-zA-Z0-9.]+
+        |  \textbackslash[ [a-f0-9:.]+ \NT{zone}? \textbackslash]    IPv6 literals (no future format).
+        |  \{ [\^{}\}]+ \}                   For Unix domain sockets only.
 
-  \NT{host} ::= [-_a-zA-Z0-9.]+
+  \NT{zone} ::= \%[-\_a-zA-Z0-9~\%.]+
 
   \NT{port} ::= [0-9]+
 \end{alltt}
@@ -1325,10 +1366,11 @@ Here are all the preferences supported by Unison.  This list can be
 \begin{quote}
 \verbatiminput{prefs.tmp}
 \end{quote}
+
 Here, in more detail, is what they do.  Many are discussed in greater detail
 in other sections of the manual.
 
-It should be noted that some command-line arguments are handled specially during startup, including \verb|-doc|, \verb|-help|, \verb|-version|, \verb|-server|, \verb|-socket|, and \verb|-ui|. They are expected to appear on the command-line only, not in a profile. In particular, \verb|-version| and \verb|-doc| will print to the standard output, so they only make sense if invoked from the command-line (and not a click-launched gui that has no standard output). Furthermore, the actions associated with these command-line arguments are executed without loading a profile or doing the usual command-line parsing. This is because we want to run the actions without loading a profile; and then we can't do command-line parsing because it is intertwined with profile loading.
+It should be noted that some command-line arguments are handled specially during startup, including \verb|-doc|, \verb|-help|, \verb|-version|, \verb|-socket|, and \verb|-ui|. They are expected to appear on the command-line only, not in a profile. In particular, \verb|-version| and \verb|-doc| will print to the standard output, so they only make sense if invoked from the command-line (and not a click-launched gui that has no standard output). Furthermore, the actions associated with these command-line arguments are executed without loading a profile or doing the usual command-line parsing.
 %
 \input{prefsdocs.tmp}
 
@@ -1357,10 +1399,16 @@ for a boolean flag or
 \end{verbatim}
 for a preference of any other type.
 
-Whitespaces around {\tt p} and {\tt xxx} are ignored.
-A profile may also include blank lines and lines beginning
+A profile may include blank lines and lines beginning
 with {\tt \#}; both are ignored.
 
+Spaces and tabs before and after {\tt p} and {\tt xxx} are ignored.
+Spaces, tabs, and non-printable characters within values are not
+treated specially, so that e.g. \verb|root = /foo bar| refers to a
+directory containing a space.
+(On systems using newline for line ending, carriage returns are
+currently ignored, but this is not part of the specification.)
+
 When Unison starts, it first reads the profile and then the command
 line, so command-line options will override settings from the
 profile.
@@ -1375,7 +1423,7 @@ A similar line of the form \texttt{source \ARG{name}} does the same except
 that it does not attempt to add a suffix to \ARG{name}.
 Similar lines of the form \texttt{include\mbox{?} \ARG{name}} or
 \texttt{source\mbox{?} \ARG{name}} do the same as their respective lines
-without the question mark except that it does not constitue an error to
+without the question mark except that it does not constitute an error to
 specify a non-existing file \ARG{name}.
 In \ARG{name} the backslash is an escape character.
 
@@ -1545,7 +1593,7 @@ preference.  For example,
 \begin{verbatim}
     backup = Name *
 \end{verbatim}
-causes Unison to keep backups of {\em all} files and directories.  The
+causes Unison to create backups of {\em all} files and directories.  The
 \verb|backupnot| preference can be used to give a few exceptions: it
 specifies which files and directories should {\em not} be backed up, even if
 they match the \verb|backup| pathspec.
@@ -1562,7 +1610,10 @@ regardless of their names.
 Backup files can be stored either {\em centrally} or {\em locally}.  This
 behavior is controlled by the preference \verb|backuplocation|, whose value
 must be either \verb|central| or \verb|local|.  (The default is
-\verb|central|.)
+\verb|central|.)  Note that central storage of backups can lead to
+backup files being stored in a different filesystem than the original
+files, which could have different security properties and different
+amounts of available storage.
 
 When backups are stored locally, they are kept in the same
 directory as the original.
@@ -1573,8 +1624,10 @@ environment variable \verb|UNISONBACKUPDIR|.  (The environment variable is
 checked first.)  If neither of these are set, then the directory
 \verb|.unison/backup| in the user's home directory is used.
 
-The preference \verb|maxbackups| controls how many previous versions of
-each file are kept (including the current version).
+The preference \verb|maxbackups| (default 2) controls how many
+previous versions of each file are kept (including the current
+version), following the usual plan of deleting the oldest when
+creating a new one.
 
 By default, backup files are named \verb|.bak.VERSION.FILENAME|,
 where \verb|FILENAME| is the original filename and \verb|VERSION| is the
@@ -1594,6 +1647,9 @@ note that the string \verb|$VERSION| in either \verb|backupprefix| or
 the version number.  This can be used, for example, to ensure that backup
 files retain the same extension as the originals.
 
+Other than \verb|maxbackups| (which will never delete the last
+backup), there are no other mechanisms for deleting backups.
+
 For backward compatibility, the \verb|backups| preference is also supported.
 %
 It simply means \verb|backup = Name *| and \verb|backuplocation = local|.
@@ -1622,15 +1678,16 @@ need to be merged, but also a file containing the {\em last synchronized
   version}.  You can ask Unison to keep a copy of the last synchronized
 version for some files using the \verb|backupcurrent| preference. This
 preference is used in exactly the same way as \verb|backup| and its meaning
-is similar, except that it causes backups to be kept of the {\em current}
+is similar, except that it causes backups to be created of the {\em current}
 contents of each file after it has been synchronized by Unison, rather than
-the {\em previous} contents that Unison overwrote.  These backups are kept
-on {\em both} replicas in the same place as ordinary backup files---i.e.
+the {\em previous} contents that Unison overwrote.  These backups are stored
+in {\em both} replicas in the same place as ordinary backup files---i.e.
 according to the \verb|backuplocation| and \verb|backupdir| preferences.
 They are named like the original files if \verb|backupslocation| is set to
 'central' and otherwise, Unison uses the \verb|backupprefix| and
 \verb|backupsuffix| preferences and assumes a version number 000 for these
-backups.
+backups.  Note that there are no mechanisms (beyond the limit on the number of
+backups for each file) to remove backup files.
 
 The \verb|<MERGECMD>| part of the preference specifies what external command
 should be invoked to merge files at paths matching the \verb|<PATHSPEC>|.
@@ -1813,6 +1870,41 @@ Setting the \verb|dumbtty| preference will force Unison to leave the
 terminal alone and process input a line at a time.
 \end{itemize}
 
+\SUBSECTION{Interrupting a Synchronization}{intr}
+
+It is possible to interrupt an ongoing synchronization process before it
+completes. Different user interfaces offer different ways of doing it.
+
+\begin{tkui}
+In the graphical user interface the synchronization process can be interrupted
+before it is finished by pressing the ``Stop'' button or by closing the window.
+The ``Stop'' button causes the onging propagation to be stopped as quickly as
+possible while still doing proper cleanup. The application keeps running and a
+rescan can be performed or a different profile selected. Closing the window in
+the middle of update propagation process will exit the application immediately
+without doing proper cleanup; it is therefore not recommended unless the
+``Stop'' button does not react quickly enough.
+\end{tkui}
+
+\begin{textui}
+When not synchronizing continuously, the text interface terminates when
+synchronization is finished normally or due to a fatal error occurring.
+
+In the text interface, to interrupt synchronization before it is finished,
+press ``Ctrl-C'' (or send signal \verb|SIGINT| or \verb|SIGTERM|). This will
+interrupt update propagation as quickly as possible but still complete proper
+cleanup. If the process does not stop even after pressing ``Ctrl-C'' then keep
+doing it repeatedly.  This will bypass cleanup procedures and terminates the
+process forcibly (similar to \verb|SIGKILL|).  Doing so may leave the archives
+or replicas in an inconsistent state or locked.
+
+When synchronizing continuously (time interval repeat or with filesystem
+monitoring), interrupting with ``Ctrl-C'' or with signal \verb|SIGINT| or
+\verb|SIGTERM| works the same way as described above and will additionally stop
+the continuous process. To stop only the continuous process and let the last
+synchronization complete normally, send signal \verb|SIGUSR2| instead.
+\end{textui}
+
 \SUBSECTION{Exit Code}{exit}
 
 When running in the textual mode, Unison returns an exit status, which
@@ -2034,12 +2126,157 @@ propagated.  The values of the other bits are set to default values
 Unix system).
 \item For security reasons, the Unix \verb|setuid| and \verb|setgid|
 bits are not propagated.
-\item The Unix owner and group ids are not propagated.  (What would
-this mean, in general?)  All files are created with the owner and
-group of the server process.
+\item The Unix owner and group ids can be propagated (see \verb|owner|
+and \verb|group| preferences) by mapping names or by numeric ids (see
+\verb|numericids| preference).
 \end{itemize}
 
 
+\SUBSECTION{Access Control Lists - ACLs}{acls}
+
+Unison allows synchronizing access control lists (ACLs) on platforms
+and filesystems that support them. In general, synchronization makes
+sense only in case both replicas support the same type of ACLs and
+recognize same users and groups. In some cases you may be able to
+go beyond that and synchronize ACLs to a replica that couldn't fully
+use them---this may be be useful for the purpose of preserving ACLs.
+
+If one of the replicas does not support any type of ACLs then
+Unison will not attempt ACL synchronization. If the other replica
+does support ACLs then those will remain intact.
+
+If both replicas support ACLs of any supported type then you can
+request Unison to try ACL synchronization (\verb|acl| preference).
+Success of synchronization depends on permissions of the owner and
+group of Unison process (Unison must have permissions to set ACL)
+and the compatibility of ACL types on both replicas.
+
+An ACL is propagated as a single unit, with all ACEs. There is no
+merging of ACEs from the replicas.
+
+{\em Caveat}: ACE inheritance may in certain scenarios cause synchronization
+inconsistencies. In Windows, only explicit ACEs are synchronized; inherited
+ACEs are not actively synchronized, but Windows will propagate ACEs from parent
+directories (unless inheritance is explicitly prevented on a file or a
+directory---this prevention is also synchronized). Due to inheritance, the
+ultimately effective ACL may be different, or provide different access, even
+after synchronization.
+
+Unison currently supports the following platforms and ACL types:
+\begin{itemize}
+  \item Windows (Windows XP SP2 and later)
+  \begin{itemize}
+    \item NTFS ACL (discrete ACL (DACL) only)
+  \end{itemize}
+\item Solaris, OpenSolaris and illumos-based OS (OpenIndiana, SmartOS,
+  OmniOS, etc.)
+  \begin{itemize}
+  \item NFSv4 ACL (ZFS ACL)
+  \item POSIX-draft ACL
+  \item Some NFSv4 ACL (ZFS ACL) cross-synchronization with
+  POSIX-draft ACL
+  \item Full cross-synchronization with other platforms that support
+  NFSv4 ACLs; limited cross-synchronization with POSIX-draft ACLs
+  \end{itemize}
+\item FreeBSD, NetBSD
+  \begin{itemize}
+  \item NFSv4 ACL (ZFS ACL)
+  \item Limited POSIX-draft ACL (access ACL only; not default ACL)
+  \item Full cross-synchronization with other platforms that support
+  NFSv4 ACLs
+  \end{itemize}
+\item Darwin (macOS)
+  \begin{itemize}
+  \item Extended ACL
+  \end{itemize}
+\end{itemize}
+Not all filesystems on the listed platforms support all ACL types
+(or any ACLs at all).
+
+Synchronizing POSIX ACLs on Linux is not supported directly. However, it is
+possible to synchronize these ACLs with another Linux system by synchronizing
+extended attributes (xattrs) instead, because POSIX ACLs are stored as xattrs
+by Linux. This is disabled by default (see \sectionref{xattrs}{Extended
+Attributes - xattrs}).  A simple way to enable syncing POSIX ACLs on Linux is
+to enable the preference \verb|xattrs| and add a preference
+\verb|xattrignorenot| with a value \texttt{Path !system.posix\_acl\_*}.  The
+\verb|*| will be expanded to include both \verb|posix_acl_access| and
+\verb|posix_acl_default| attributes -- if you only want to sync either one,
+just remove the \verb|*| and type out the attribute name in full.  If you want
+to prevent other xattrs from being synced then add an \verb|xattrignore| with a
+value \texttt{Path *} (value \texttt{Regex .*} will also work).
+
+
+\SUBSECTION{Extended Attributes - xattrs}{xattrs}
+
+Unison allows synchronizing extended attributes on platforms and
+filesystems that support them. System attributes are not synchronized.
+What exactly is considered a system attribute is platform-dependent.
+Synchronization is possible cross-platform, but see caveats below.
+
+If one of the replicas does not support extended attributes then
+Unison will not attempt attribute synchronization. If the other
+replica does support extended attributes then those will remain intact.
+
+If both replicas support extended attributes then you can request
+Unison to try attribute synchronization (\verb|xattrs| preference).
+Extended attributes from both replicas will not be merged, all extended
+attributes are propagated as a set from one replica to another.
+
+Unison currently supports extended attributes on the following platforms:
+\begin{itemize}
+\item {\em Linux}
+Attributes in user, trusted and security namespaces. Synchronization of
+the latter two namespaces depends on \verb|unison| process privileges
+and is disabled by default. To sync one or more attributes in the security
+namespace, for example, you can set the preference
+\verb|xattrignorenot| to \verb|Path !security.*| (for all) or to
+\verb|Path !security.selinux| (for one specific attribute).
+Attributes in system namespace are not synchronized, with the exception of
+\verb|system.posix_acl_default| and \verb|system.posix_acl_access| (also
+disabled by default).
+\item {\em Solaris, OpenSolaris and illumos-based OS (OpenIndiana, SmartOS,
+  OmniOS, etc.)}
+\item {\em FreeBSD, NetBSD}
+Attributes in user namespace.
+\item {\em Darwin (macOS)}
+\end{itemize}
+Not all filesystems on the listed platforms may support extended attributes.
+
+\noindent {\it Caveats:}
+\begin{itemize}
+\item Some platforms and file systems support very large extended attribute
+values. Unison synchronizes only up to 16 MB of each attribute value.
+\item Attributes are synchronized as simple name-value pairs. More complex
+extended attribute concepts supported by some platforms are not synchronized.
+\item On Linux, attribute names always have a fully qualified form
+(\texttt{namespace.attribute}). Other platforms do not have the same constraint.
+The consequence of this is that Unison will sync the attribute names on Linux
+as follows: an \verb|!| is prepended to the namespace name, except for the
+\verb|user| namespace; the \verb|user.| prefix is stripped from attribute names
+instead. This allows syncing extended attributes from Linux to other platforms.
+These transformations are reversed when syncing {\em to} Linux, resulting in
+correct fully qualified attribute names.
+The \verb|xattrignore| and \verb|xattrignorenot| preferences work on the
+transformed attribute names. This means that any patterns for the user
+namespace must be specified without the \verb|user.| prefix and any patterns
+intended for other namespaces must begin with an \verb|!|.
+\end{itemize}
+
+The \verb|xattrignore| preference can be used to filter the names of extended
+attributes that will be synchronized. The most useful ignore patterns can
+be constructed with the \verb|Path| form (where shell wildcards \verb|*| and
+\verb|?| are supported) and with the \verb|Regex| form. The
+\verb|xattrignorenot| preference can be used to override \verb|xattrignore|.
+
+Disabling the security and trusted namespaces on Linux is achieved by setting
+a default \verb|xattrignore| pattern of
+\texttt{Regex !(security|trusted)[.].*}.
+Disabling the syncing of attributes used to store POSIX ACL on Linux is
+achieved by setting a default \verb|xattrignore| pattern of
+\texttt{Path !system.posix\_acl\_*}.
+
+
 \SUBSECTION{Cross-Platform Synchronization}{crossplatform}
 
 If you use Unison to synchronize files between Windows and Unix
@@ -2123,14 +2360,6 @@ are:
    copyprog      =   rsync --inplace --compress
    copyprogrest  =   rsync --partial --inplace --compress
 \end{verbatim}
-You may also need to set the {\tt copyquoterem} preference.  When it is set
-to {\tt true}, this causes Unison to add an extra layer of quotes to
-the remote path passed to the external copy program. This is is needed by
-rsync, for example, which internally uses an ssh connection, requiring an
-extra level of quoting for paths containing spaces. When this flag is set to
-{\tt default}, extra quotes are added if the value of {\tt copyprog}
-contains the string {\tt rsync}.  The default value is {\tt default},
-naturally.
 
 If a {\em directory} transfer is interrupted, the next run of Unison will
 automatically skip any files that were completely transferred before the
@@ -2215,15 +2444,6 @@ To use Unison in this mode, you must first create a profile (see
 
 
 \appendix
-\SECTION{Ssh}{ssh}{ssh}
-
-Your local host will need just an ssh client; the remote host needs an
-ssh server (or daemon).   ssh is now normal, and Unison thus does not
-provide instructions.
-
-\SECTION{Changes in Version \unisonversion}{news}{news}
-
-\input{changes.tex}
 
 \finishlater{
 \SECTION{Other Synchronizers}{other}{other}
diff --git a/dune-project b/dune-project
index 4458215..a20112a 100644
--- a/dune-project
+++ b/dune-project
@@ -1,2 +1,76 @@
 (lang dune 2.3)
+
 (name unison)
+
+(version dev)
+
+(generate_opam_files false)
+
+(license GPL-3.0-or-later)
+
+(maintainers "unison-hackers@lists.seas.upenn.edu")
+
+(authors "Trevor Jim" "Benjamin C. Pierce" "J\195\169r\195\180me Vouillon")
+
+(source
+ (github bcpierce00/unison))
+
+(homepage https://www.cis.upenn.edu/~bcpierce/unison/)
+
+(documentation "https://github.com/bcpierce00/unison/wiki")
+
+(package
+ (name unison)
+ (synopsis "File-synchronization tool for Unix and Windows")
+ (description "\
+Text based user interface for Unison.
+
+Unison is a file-synchronization tool for Unix and Windows.
+It allows two replicas of a collection of files and directories
+to be stored on different hosts (or different disks on the same host),
+modified separately, and then brought up to date by propagating
+the changes in each replica to the other.
+")
+ (depends
+  (ocaml
+   (>= 4.08))
+  (dune
+   (>= 2.3))))
+
+(package
+ (name unison-gui)
+ (synopsis "File-synchronization tool for Unix and Windows")
+ (description "\
+Graphical user interface for Unison.
+
+Unison is a file-synchronization tool for Unix and Windows.
+It allows two replicas of a collection of files and directories
+to be stored on different hosts (or different disks on the same host),
+modified separately, and then brought up to date by propagating
+the changes in each replica to the other.
+")
+ (depends
+  (ocaml
+   (>= 4.08))
+  (dune
+   (>= 2.3))
+  lablgtk))
+
+(package
+ (name unison-fsmonitor)
+ (synopsis "File-synchronization tool for Unix and Windows")
+ (description "\
+This optional add-on for unison monitors file system changes
+on all given (relative to root) paths.
+
+Unison is a file-synchronization tool for Unix and Windows.
+It allows two replicas of a collection of files and directories
+to be stored on different hosts (or different disks on the same host),
+modified separately, and then brought up to date by propagating
+the changes in each replica to the other.
+")
+ (depends
+  (ocaml
+   (>= 4.08))
+  (dune
+   (>= 2.3))))
diff --git a/icons/unison.png b/icons/unison.png
new file mode 100644
index 0000000..1994a53
Binary files /dev/null and b/icons/unison.png differ
diff --git a/icons/unison.svg b/icons/unison.svg
new file mode 100644
index 0000000..6586a4c
--- /dev/null
+++ b/icons/unison.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="207px" height="52px" viewBox="0 0 207 52" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Unison</title>
+    <defs>
+        <linearGradient x1="49.9999257%" y1="31.7505524%" x2="49.9999257%" y2="69.6081184%" id="linearGradient-1">
+            <stop stop-color="#F9F90A" offset="0%"></stop>
+            <stop stop-color="#FF3201" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M38.315852,1.168 L30.184852,10.193 C29.889852,10.52 30.121852,11.042 30.561852,11.042 L30.561852,11.042 L32.982852,11.042 C33.262852,11.042 33.490852,11.27 33.490852,11.551 L33.490852,11.551 L33.490852,31.41 C33.490852,34.932 33.099852,37.653 32.317852,39.57 L32.317852,39.57 C31.534852,41.487 30.291852,42.992 28.585852,44.085 L28.585852,44.085 C27.285852,44.919 25.834852,45.425 24.241852,45.623 L24.241852,45.623 C24.190852,45.629 24.136852,45.629 24.085852,45.623 L24.085852,45.623 C22.493852,45.425 21.042852,44.919 19.742852,44.085 L19.742852,44.085 C18.035852,42.992 16.792852,41.487 16.010852,39.57 L16.010852,39.57 C15.227852,37.653 14.836852,34.932 14.836852,31.41 L14.836852,31.41 L14.836852,11.551 C14.836852,11.27 15.063852,11.042 15.345852,11.042 L15.345852,11.042 L17.764852,11.042 C18.205852,11.042 18.437852,10.52 18.142852,10.193 L18.142852,10.193 L10.012852,1.168 C9.80985201,0.944 9.45785201,0.944 9.25685201,1.169 L9.25685201,1.169 L1.13185201,10.193 C0.836852007,10.521 1.06985201,11.042 1.50985201,11.042 L1.50985201,11.042 L3.92985201,11.042 C4.20985201,11.042 4.43785201,11.27 4.43785201,11.551 L4.43785201,11.551 L4.43185201,31.41 C4.44985201,32.987 4.52485201,34.353 4.66485201,35.557 L4.66485201,35.557 C4.82485201,36.939 5.06585201,38.21 5.38785201,39.369 L5.38785201,39.369 C6.42985201,42.938 8.33685201,45.657 11.104852,47.529 L11.104852,47.529 C12.829852,48.734 14.776852,49.627 16.943852,50.205 L16.943852,50.205 C19.109852,50.785 21.516852,51.075 24.163852,51.075 L24.163852,51.075 C26.810852,51.075 29.218852,50.785 31.383852,50.205 L31.383852,50.205 C33.550852,49.627 35.497852,48.734 37.222852,47.529 L37.222852,47.529 C39.991852,45.657 41.897852,42.938 42.940852,39.369 L42.940852,39.369 C43.262852,38.21 43.503852,36.939 43.663852,35.557 L43.663852,35.557 C43.802852,34.353 43.877852,32.987 43.895852,31.41 L43.895852,31.41 L43.890852,11.551 C43.889852,11.27 44.116852,11.042 44.398852,11.042 L44.398852,11.042 L46.817852,11.042 C47.258852,11.042 47.491852,10.521 47.195852,10.193 L47.195852,10.193 L39.070852,1.169 C38.969852,1.056 38.831852,1 38.692852,1 L38.692852,1 C38.554852,1 38.416852,1.056 38.315852,1.168 L38.315852,1.168 Z M90.934852,5.122 C89.810852,6.193 89.249852,7.464 89.249852,8.935 L89.249852,8.935 C89.249852,10.362 89.810852,11.588 90.934852,12.613 L90.934852,12.613 C92.058852,13.64 93.362852,14.152 94.848852,14.152 L94.848852,14.152 C96.493852,14.152 97.846852,13.662 98.909852,12.681 L98.909852,12.681 C99.973852,11.7 100.505852,10.473 100.505852,9.002 L100.505852,9.002 C100.505852,7.396 99.983852,6.082 98.941852,5.055 L98.941852,5.055 C97.896852,4.03 96.593852,3.516 95.028852,3.516 L95.028852,3.516 C93.422852,3.516 92.058852,4.052 90.934852,5.122 L90.934852,5.122 Z M186.230852,19.218 C185.464852,19.703 184.821852,20.187 184.298852,20.671 L184.298852,20.671 C183.775852,21.154 183.152852,21.881 182.427852,22.848 L182.427852,22.848 L182.427852,21.263 C182.427852,20.34 182.146852,19.505 181.584852,18.756 L181.584852,18.756 C181.302852,18.405 181.061852,18.174 180.862852,18.063 L180.862852,18.063 C180.660852,17.953 180.440852,17.898 180.199852,17.898 L180.199852,17.898 L178.936852,18.164 L171.770852,20.969 C170.645852,21.503 170.084852,22.238 170.084852,23.172 L170.084852,23.172 C170.084852,24.062 170.593852,24.729 171.617852,25.175 L171.617852,25.175 C172.761852,25.53 173.334852,26.509 173.334852,28.111 L173.334852,28.111 L173.334852,43.13 C173.334852,44.643 172.752852,45.667 171.589852,46.201 L171.589852,46.201 C170.584852,46.603 170.084852,47.27 170.084852,48.204 L170.084852,48.204 C170.084852,49.494 170.783852,50.139 172.183852,50.139 L172.183852,50.139 L184.235852,50.139 C185.474852,50.139 186.094852,49.494 186.094852,48.205 L186.094852,48.205 C186.094852,47.226 185.592852,46.559 184.589852,46.202 L184.589852,46.202 C184.147852,46.024 183.856852,45.836 183.716852,45.635 L183.716852,45.635 C183.576852,45.435 183.465852,45.125 183.385852,44.702 L183.385852,44.702 C183.305852,44.28 183.265852,43.802 183.265852,43.267 L183.265852,43.267 L183.265852,28.921 C183.265852,27.543 183.854852,26.297 185.031852,25.185 L185.031852,25.185 C186.208852,24.073 187.508852,23.517 188.930852,23.517 L188.930852,23.517 C190.554852,23.517 191.752852,24.14 192.524852,25.385 L192.524852,25.385 C192.889852,26.052 193.131852,26.675 193.254852,27.254 L193.254852,27.254 C193.377852,27.832 193.437852,28.856 193.437852,30.323 L193.437852,30.323 L193.437852,33.392 L193.437852,37.996 C193.437852,39.596 193.417852,40.921 193.377852,41.966 L193.377852,41.966 C193.337852,43.011 193.296852,43.867 193.256852,44.535 L193.256852,44.535 C193.137852,45.335 192.644852,45.902 191.782852,46.236 L191.782852,46.236 C190.919852,46.57 190.489852,47.226 190.489852,48.205 L190.489852,48.205 C190.489852,49.494 191.170852,50.139 192.535852,50.139 L192.535852,50.139 L204.392852,50.139 C205.033852,50.139 205.545852,49.962 205.927852,49.605 L205.927852,49.605 C206.308852,49.25 206.499852,48.783 206.499852,48.204 L206.499852,48.204 C206.499852,47.27 206.065852,46.624 205.199852,46.268 L205.199852,46.268 C204.333852,45.912 203.811852,45.468 203.633852,44.933 L203.633852,44.933 C203.457852,44.399 203.369852,43.086 203.369852,40.994 L203.369852,40.994 L203.369852,38.591 L203.369852,35.588 C203.369852,31.494 203.268852,28.657 203.067852,27.077 L203.067852,27.077 C202.866852,25.496 202.462852,24.083 201.860852,22.837 L201.860852,22.837 C200.049852,19.545 196.689852,17.898 191.781852,17.898 L191.781852,17.898 C189.729852,17.898 187.880852,18.338 186.230852,19.218 L186.230852,19.218 Z M140.170852,22.562 C137.041852,25.671 135.475852,29.602 135.475852,34.353 L135.475852,34.353 C135.475852,39.195 136.999852,43.17 140.049852,46.278 L140.049852,46.278 C143.099852,49.387 147.031852,50.942 151.847852,50.942 L151.847852,50.942 C156.622852,50.942 160.554852,49.387 163.645852,46.278 L163.645852,46.278 C166.733852,43.17 168.279852,39.217 168.279852,34.42 L168.279852,34.42 C168.279852,29.623 166.733852,25.671 163.645852,22.562 L163.645852,22.562 C160.554852,19.453 156.662852,17.898 151.967852,17.898 L151.967852,17.898 C147.232852,17.898 143.300852,19.453 140.170852,22.562 L140.170852,22.562 Z M146.701852,43.301 C145.476852,41.195 144.865852,38.235 144.865852,34.42 L144.865852,34.42 C144.865852,30.606 145.476852,27.646 146.701852,25.539 L146.701852,25.539 C147.924852,23.433 149.640852,22.38 151.847852,22.38 L151.847852,22.38 C154.093852,22.38 155.840852,23.445 157.084852,25.572 L157.084852,25.572 C158.326852,27.702 158.949852,30.628 158.949852,34.353 L158.949852,34.353 C158.949852,38.168 158.326852,41.14 157.084852,43.267 L157.084852,43.267 C155.840852,45.397 154.093852,46.46 151.847852,46.46 L151.847852,46.46 C149.640852,46.46 147.924852,45.408 146.701852,43.301 L146.701852,43.301 Z M109.172852,22.359 C107.728852,24.223 107.006852,26.354 107.006852,28.75 L107.006852,28.75 C107.006852,30.171 107.276852,31.381 107.818852,32.379 L107.818852,32.379 C108.359852,33.378 109.312852,34.431 110.677852,35.542 L110.677852,35.542 C111.881852,36.385 113.025852,36.962 114.109852,37.272 L114.109852,37.272 L119.044852,38.604 C119.925852,38.871 120.549852,39.091 120.909852,39.27 L120.909852,39.27 C121.270852,39.447 121.591852,39.692 121.873852,40.001 L121.873852,40.001 C122.514852,40.579 122.836852,41.378 122.836852,42.399 L122.836852,42.399 C122.836852,43.509 122.354852,44.44 121.391852,45.195 L121.391852,45.195 C120.428852,45.949 119.264852,46.326 117.900852,46.326 L117.900852,46.326 C115.531852,46.326 113.587852,45.269 112.062852,43.151 L112.062852,43.151 L110.737852,41.365 C110.496852,41.101 110.265852,40.902 110.045852,40.769 L110.045852,40.769 C109.825852,40.637 109.614852,40.527 109.413852,40.44 L109.413852,40.44 L108.450852,40.306 C107.728852,40.306 107.126852,40.594 106.644852,41.17 L106.644852,41.17 C106.163852,41.748 105.922852,42.456 105.922852,43.298 L105.922852,43.298 C105.922852,45.38 107.206852,47.175 109.774852,48.683 L109.774852,48.683 C112.342852,50.188 115.351852,50.942 118.803852,50.942 L118.803852,50.942 C122.776852,50.942 126.116852,49.854 128.824852,47.68 L128.824852,47.68 C131.533852,45.505 132.888852,42.82 132.888852,39.624 L132.888852,39.624 C132.888852,37.582 131.799852,35.606 129.758852,33.565 L129.758852,33.565 C129.391852,33.197 128.167852,32.091 126.567852,31.501 L126.567852,31.501 L121.451852,29.97 C120.166852,29.659 119.234852,29.315 118.653852,28.938 L118.653852,28.938 C118.069852,28.561 117.629852,28.051 117.329852,27.407 L117.329852,27.407 C117.027852,26.763 116.877852,26.22 116.877852,25.776 L116.877852,25.776 C116.877852,23.511 118.221852,22.38 120.909852,22.38 L120.909852,22.38 C123.757852,22.38 125.865852,23.658 127.230852,26.214 L127.230852,26.214 C127.630852,26.92 128.012852,27.405 128.374852,27.669 L128.374852,27.669 C128.734852,27.934 129.215852,28.065 129.817852,28.065 L129.817852,28.065 C131.101852,28.065 131.744852,27.313 131.744852,25.805 L131.744852,25.805 C131.744852,23.016 131.322852,21.309 130.479852,20.688 L130.479852,20.688 C129.355852,19.803 127.891852,19.117 126.086852,18.63 L126.086852,18.63 C124.280852,18.143 122.393852,17.898 120.428852,17.898 L120.428852,17.898 C115.211852,17.898 111.460852,19.385 109.172852,22.359 L109.172852,22.359 Z M96.652852,18.099 L88.768852,20.909 C87.644852,21.354 87.082852,22.09 87.082852,23.115 L87.082852,23.115 C87.082852,24.008 87.585852,24.677 88.587852,25.122 L88.587852,25.122 C89.710852,25.48 90.273852,26.483 90.273852,28.133 L90.273852,28.133 L90.273852,43.049 C90.273852,43.986 90.152852,44.678 89.912852,45.122 L89.912852,45.122 C89.671852,45.569 89.139852,45.958 88.316852,46.293 L88.316852,46.293 C87.494852,46.628 87.082852,47.262 87.082852,48.199 L87.082852,48.199 C87.082852,49.493 87.784852,50.139 89.189852,50.139 L89.189852,50.139 L101.288852,50.139 C101.929852,50.139 102.441852,49.962 102.823852,49.604 L102.823852,49.604 C103.203852,49.248 103.394852,48.779 103.394852,48.199 L103.394852,48.199 C103.394852,47.262 102.963852,46.628 102.100852,46.293 L102.100852,46.293 C101.236852,45.958 100.706852,45.58 100.505852,45.156 L100.505852,45.156 C100.304852,44.733 100.204852,44.008 100.204852,42.982 L100.204852,42.982 L100.204852,22.982 C100.204852,19.594 99.441852,17.898 97.917852,17.898 L97.917852,17.898 L96.652852,18.099 Z M63.865852,19.218 C63.098852,19.703 62.456852,20.187 61.933852,20.671 L61.933852,20.671 C61.410852,21.154 60.787852,21.881 60.063852,22.848 L60.063852,22.848 L60.063852,21.263 C60.063852,20.34 59.781852,19.505 59.219852,18.756 L59.219852,18.756 C58.938852,18.405 58.696852,18.174 58.496852,18.063 L58.496852,18.063 C58.295852,17.953 58.075852,17.898 57.834852,17.898 L57.834852,17.898 L56.570852,18.164 L49.405852,20.969 C48.280852,21.503 47.718852,22.238 47.718852,23.172 L47.718852,23.172 C47.718852,24.062 48.229852,24.729 49.252852,25.175 L49.252852,25.175 C50.396852,25.53 50.969852,26.509 50.969852,28.111 L50.969852,28.111 L50.969852,43.13 C50.969852,44.643 50.387852,45.667 49.223852,46.201 L49.223852,46.201 C48.220852,46.603 47.718852,47.27 47.718852,48.204 L47.718852,48.204 C47.718852,49.494 48.418852,50.139 49.817852,50.139 L49.817852,50.139 L61.871852,50.139 C63.110852,50.139 63.729852,49.494 63.729852,48.205 L63.729852,48.205 C63.729852,47.226 63.227852,46.559 62.224852,46.202 L62.224852,46.202 C61.783852,46.024 61.492852,45.836 61.351852,45.635 L61.351852,45.635 C61.210852,45.435 61.101852,45.125 61.020852,44.702 L61.020852,44.702 C60.940852,44.28 60.901852,43.802 60.901852,43.267 L60.901852,43.267 L60.901852,28.921 C60.901852,27.543 61.489852,26.297 62.666852,25.185 L62.666852,25.185 C63.844852,24.073 65.143852,23.517 66.564852,23.517 L66.564852,23.517 C68.190852,23.517 69.388852,24.14 70.160852,25.385 L70.160852,25.385 C70.524852,26.052 70.766852,26.675 70.889852,27.254 L70.889852,27.254 C71.011852,27.832 71.072852,28.856 71.072852,30.323 L71.072852,30.323 L71.072852,33.392 L71.072852,37.996 C71.072852,39.596 71.052852,40.921 71.012852,41.966 L71.012852,41.966 C70.972852,43.011 70.932852,43.867 70.891852,44.535 L70.891852,44.535 C70.772852,45.335 70.279852,45.902 69.417852,46.236 L69.417852,46.236 C68.554852,46.57 68.123852,47.226 68.123852,48.205 L68.123852,48.205 C68.123852,49.494 68.804852,50.139 70.170852,50.139 L70.170852,50.139 L82.027852,50.139 C82.669852,50.139 83.180852,49.962 83.562852,49.605 L83.562852,49.605 C83.942852,49.25 84.133852,48.783 84.133852,48.204 L84.133852,48.204 C84.133852,47.27 83.700852,46.624 82.834852,46.268 L82.834852,46.268 C81.968852,45.912 81.446852,45.468 81.269852,44.933 L81.269852,44.933 C81.092852,44.399 81.004852,43.086 81.004852,40.994 L81.004852,40.994 L81.004852,38.591 L81.004852,35.588 C81.004852,31.494 80.903852,28.657 80.701852,27.077 L80.701852,27.077 C80.501852,25.496 80.098852,24.083 79.494852,22.837 L79.494852,22.837 C77.684852,19.545 74.325852,17.898 69.416852,17.898 L69.416852,17.898 C67.364852,17.898 65.514852,18.338 63.865852,19.218 L63.865852,19.218 Z" id="Unison" stroke="#000000" fill="url(#linearGradient-1)"></path>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/man/Makefile b/man/Makefile
new file mode 100644
index 0000000..00829c8
--- /dev/null
+++ b/man/Makefile
@@ -0,0 +1,22 @@
+-include ../src/Makefile.ProjectInfo
+
+all:: $(NAME).1
+
+../src/$(NAME)$(EXEC_EXT):
+	$(MAKE) -C ../src UISTYLE=text
+
+$(NAME).1: $(NAME).1.in opt_short.tmp opt_full.tmp
+	sed -e '/@OPTIONS_SHORT@/r ./opt_short.tmp' \
+		-e '/@OPTIONS_SHORT@/d' \
+		-e '/@OPTIONS_FULL@/r ./opt_full.tmp' \
+		-e '/@OPTIONS_FULL@/d' $(NAME).1.in > $(NAME).1
+
+# Listing of preferences
+opt_short.tmp: ../src/$(NAME)$(EXEC_EXT)
+	../src/$(NAME)$(EXEC_EXT) -prefsman short > opt_short.tmp
+
+opt_full.tmp: ../src/$(NAME)$(EXEC_EXT)
+	../src/$(NAME)$(EXEC_EXT) -prefsman full > opt_full.tmp
+
+clean::
+	$(RM) *.tmp $(NAME).1
diff --git a/man/unison.1.in b/man/unison.1.in
new file mode 100644
index 0000000..e2397e5
--- /dev/null
+++ b/man/unison.1.in
@@ -0,0 +1,518 @@
+.\" Unison file synchronizer: man/unison.1
+.\" Copyright 1999-2022, Unison authors
+.\"
+.\" 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.
+.\"
+.\" 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 General Public License for more details.
+.\"
+.\" You should have received a copy of the GNU General Public License
+.\" along with this program.  If not, see <http://www.gnu.org/licenses/>.
+.\"
+.Dd March 17, 2022
+.Os unison
+.Dt UNISON 1 URM
+.Sh NAME
+.Nm unison
+.Nd a multi-platform bi-directional file synchronization tool
+.Sh SYNOPSIS
+.Nm
+.Op Ar options
+.Nm
+.Ar root1 root2
+.Op Ar options
+.Nm
+.Ar profilename
+.Op Ar options
+.Sh DESCRIPTION
+.Nm Unison
+is a file-synchronization tool for POSIX-compliant systems (e.g. *BSD and
+GNU/Linux), macOS and Windows. It allows two replicas of a collection of files
+and directories to be stored on different hosts (or different disks on the same
+host), modified separately, and then brought up to date by propagating the
+changes in each replica to the other.
+.Pp
+Unison has been in use for over 20 years and many people use it to synchronize
+data they care about.
+.Pp
+Unison shares a number of features with other tools. Some of the distinguishing
+features are:
+.Bl -bullet -compact
+.It
+Unlike simple mirroring or backup utilities, Unison can deal with updates to
+both replicas of a distributed directory structure.
+.It
+Unison works across platforms, allowing you to synchronize a Windows laptop
+with a Unix server, for example.
+.It
+Unlike most distributed filesystems, Unison is a user-level program that simply
+uses normal systems calls: there is no need to modify the kernel, to have
+superuser privileges on either host, or to have a FUSE implementation.
+.It
+Unison works between any pair of machines connected to the internet, typically
+communicating over
+.Xr ssh 1 ,
+but also directly over TCP. It is careful with network bandwidth, and runs well
+over slow links. Transfers of small updates to large files are optimized using
+a compression protocol similar to
+.Xr rsync 1 .
+.It
+Unison is resilient to failure. It is careful to leave the replicas and its own
+private structures in a sensible state at all times, even in case of abnormal
+termination or communication failures.
+.El
+.Sh OPTION SUMMARY
+@OPTIONS_SHORT@
+.Sh OPTIONS
+Most of the options can be given as command line arguments or in a profile. On
+command line, but not in a profile, the options are specified with a leading
+dash. Like this:
+.Fl option .
+@OPTIONS_FULL@
+.Sh ROOTS
+A replica’s root tells Unison where to find a set of files to be synchronized,
+either on the local machine or on
+a remote host. For example,
+.Pp
+.Dl relative/path/of/root
+.Pp
+specifies a local root relative to the directory where Unison is started, while
+.Pp
+.Dl /absolute/path/of/root
+.Pp
+specifies a root relative to the top of the local filesystem, independent of
+where Unison is running. Remote roots can begin with
+.Sy ssh://
+to indicate that the remote server should be started with
+.Xr ssh 1 :
+.Pp
+.Dl ssh://remotehost//absolute/path/of/root
+.Dl ssh://user@remotehost/relative/path/of/root
+.Pp
+If the remote server is already running (in the socket mode), then the syntax
+.Pp
+.Dl socket://remotehost:portnum//absolute/path/of/root
+.Dl socket://remotehost:portnum/relative/path/of/root
+.Dl socket://[IPv6literal]:portnum/path
+.Pp
+is used to specify the hostname and the port that the client Unison should use
+to contact it. Syntax
+.Pp
+.Dl socket://{path/of/socket}//absolute/path/of/root
+.Dl socket://{path/of/socket}/relative/path/of/root
+.Pp
+is used to specify the Unix domain socket the client Unison should use to
+contact the server.
+.Pp
+The syntax for roots is based on that of URIs (described in RFC 2396). The full
+grammar is:
+.Bd -literal
+  replica ::= [protocol:]//[user@][host][:port][/path]
+           |  path
+
+  protocol ::= file
+            |  socket
+            |  ssh
+
+  user ::= [-_a-zA-Z0-9]+
+
+  host ::= [-_a-zA-Z0-9.]+
+        |  \e[ [a-f0-9:.]+ zone? \e]     IPv6 literals (no future format).
+        |  { [^}]+ }                   For Unix domain sockets only.
+
+  zone ::= %[-_a-zA-Z0-9~%.]+
+
+  port ::= [0-9]+
+
+.Ed
+When path is given without any protocol prefix, the protocol is assumed to be
+.Sy file: .
+Under Windows, it is possible to synchronize with a remote directory using the
+.Sy file:
+protocol over the Windows Network Neighborhood. For example,
+.Pp
+.Dl unison foo //host/drive/bar
+.Pp
+synchronizes the local directory
+.Pa foo
+with the directory
+.Pa drive:\ebar
+on the machine
+.Sy host ,
+provided that host is accessible via Network Neighborhood. When the
+.Sy file:
+protocol is used in this way, there is no need for a Unison server to be
+running on the remote host. However, running Unison this way is only a good
+idea if the remote host is reached by a very fast network connection, since the
+full contents of every file in the remote replica will have to be transferred
+to the local machine to detect updates.
+.Sh PATHS
+A path refers to a point within a set of files being synchronized; it is
+specified relative to the root of the replica. Formally, a path is just a
+sequence of names, separated by /. Note that the path separator character is
+always a forward slash, no matter what operating system Unison is running on.
+The empty path (i.e., the empty sequence of names) denotes the whole replica.
+.Sh PATH SPECIFICATION
+Several Unison preferences (e.g.,
+.Sy ignore/ignorenot , follow , sortfirst/sortlast , backup , merge ,
+etc.) specify individual paths or sets of paths. These preferences share a
+common syntax based on regular expressions. Each preference is associated with
+a list of path patterns; the paths specified are those that match any one of
+the path pattern.
+.Pp
+Each pattern can have one of three forms. The most general form is a Posix
+extended regular expression introduced by the keyword
+.Sy Regex .
+(The collating sequences and character classes of full Posix regexps are not
+currently supported.)
+.Pp
+.Dl Cm Regex Ar regexp
+.Pp
+For convenience, three other styles of pattern are also recognized:
+.Pp
+.Dl Cm Name Ar name
+.Pp
+matches any path in which the last component matches
+.Ar name ,
+.Pp
+.Dl Cm Path Ar path
+.Pp
+matches exactly the path
+.Ar path ,
+and
+.Pp
+.Dl Cm BelowPath Ar path
+.Pp
+matches the path
+.Ar path
+and any path below. The
+.Ar name
+and
+.Ar path
+arguments of the latter forms of patterns are
+.Em not
+regular expressions. Instead, standard
+.Dq globbing
+conventions can be used in
+.Ar name
+and
+.Ar path :
+.Bl -dash
+.It
+a
+.Sy "*"
+matches any sequence of characters not including / (and not beginning with .,
+when used at the beginning of a
+.Ar name )
+.It
+a
+.Sy \&?
+matches any single character except / (and leading .)
+.It
+.Sy [xyz]
+matches any character from the set {x, y, z}
+.It
+.Sy {a,bb,ccc}
+matches any one of a, bb, or ccc. (Be careful not to put extra spaces after the
+commas: these will be interpreted literally as part of the strings to be
+matched!)
+.El
+.Pp
+The path separator in path patterns is always the forward-slash character
+.Dq /
+— even when the client or server is running under Windows, where the normal
+separator character is a backslash. This makes it possible to use the same set
+of path patterns for both Unix and Windows file systems.
+.Pp
+A path specification may be followed by the separator
+.Dq " -> "
+itself followed by a string which will be associated to the matching paths:
+.Pp
+.Dl Cm Path Ar path No -> Ar "associated string"
+.Pp
+Not all pathspec preferences use these associated strings but all pathspec
+preferences are parsed identically and the strings may be ignored. Only the
+last match of the separator string on the line is used as a delimiter. Thus to
+allow a path specification to contain the separator string, append an
+associated string to it, even if it is not used. The associated string cannot
+contain the separator string.
+.Sh PROFILES
+A profile is a text file that specifies permanent settings for roots, paths,
+ignore patterns, and other preferences, so that they do not need to be typed at
+the command line every time Unison is run. Profiles should reside in the
+.Pa .unison
+directory on the client machine. If Unison is started with just one argument
+.Ar name
+on the
+command line, it looks for a profile called
+.Em name Ns Sy .prf
+in the
+.Pa .unison
+directory. If it is started with no arguments, it scans the
+.Pa .unison
+directory for files whose names end in
+.Sy .prf
+and offers a menu (when using the graphical user interface; for the text
+interface, you have to use the
+.Fl i
+option). If a file named
+.Pa default.prf
+is found, its settings will be used as the default preferences.
+.Pp
+To set the value of a preference
+.Sy p
+permanently, add to the appropriate profile a line of the form
+.Pp
+.Dl p = true
+.Pp
+for a boolean flag or
+.Pp
+.Dl p = <value>
+.Pp
+for a preference of any other type.
+Whitespaces around
+.Sy p
+and the value are ignored. A profile may also include blank lines and lines
+beginning with #; both are ignored.
+.Pp
+When Unison starts, it first reads the profile and then the command line, so
+command-line options will override settings from the profile.
+.Sh TERMINATION
+When not synchronizing continuously, the text interface terminates when
+synchronization is finished normally or due to a fatal error occurring.
+.Pp
+In the text interface, to interrupt synchronization before it is finished,
+press
+.Sy Ctrl-C
+(or send signal
+.Sy SIGINT
+or
+.Sy SIGTERM ) .
+This will interrupt update propagation as quickly as possible but still
+complete proper cleanup. If the process does not stop even after pressing
+.Sy Ctrl-C
+then keep doing it repeatedly. This will bypass cleanup procedures and
+terminates the process forcibly (similar to
+.Sy SIGKILL ) .
+Doing so may leave the archives or replicas in an inconsistent state or locked.
+.Pp
+When synchronizing continuously (time interval repeat or with filesystem
+monitoring), interrupting with
+.Sy Ctrl-C
+or with signal
+.Sy SIGINT
+or
+.Sy SIGTERM
+works the same way as described above and will additionally stop the continuous
+process. To stop only the continuous process and let the last synchronization
+complete normally, send signal
+.Sy SIGUSR2
+instead.
+.Sh ENVIRONMENT
+.Bl -tag
+.It Ev UNISON
+Unison stores a variety of information in a private directory on each host. If
+the environment variable
+.Sy UNISON
+is defined, then its value will be used as the path for this directory. This
+can be just a name, or a path. If
+.Sy UNISON
+is not defined, then the directory depends on which operating system you are
+using. In Unix, the default is to use
+.Pa $HOME/.unison .
+In Windows, if the environment variable
+.Sy USERPROFILE
+is defined, then the directory will be
+.Pa $USERPROFILE\e.unison ;
+otherwise, it will be
+.Pa c:\e.unison .
+On macOS,
+.Pa $HOME/.unison
+will be used if it is present, but
+.Pa "$HOME/Library/Application Support/Unison"
+will be created and used by default.
+.It Ev UNISONLOCALHOSTNAME
+The function that finds the canonical hostname of the local host (which is
+used, for example, in calculating the name of the archive file used to remember
+which files have been synchronized) normally uses the
+.Sy gethostname
+operating system call. However, if the environment variable
+.Sy UNISONLOCALHOSTNAME
+is set, its value will be used instead. This makes it easier to use Unison in
+situations where a machine’s name changes frequently (e.g., because it is a
+laptop and gets moved around a lot).
+.It Ev UNISONBACKUPDIR
+When backups are stored centrally, the directory used to hold them is
+controlled by the preference
+.Sy backupdir
+and the environment variable
+.Sy UNISONBACKUPDIR .
+If both are specified then the environment variable overrides the preference.
+If neither of these are set, then the directory
+.Pa $UNISON/backup
+is used (see environment variable
+.Sy UNISON
+above).
+.It Ev PAGER
+Used by the text interface as the pager when displaying the differences between
+changed files.
+.It Ev NO_COLOR
+If the environment variable
+.Sy NO_COLOR
+is set then Unison's text interface will not produce any color output by
+default. The
+.Sy color
+preference overrides this environment variable.
+.El
+.Sh FILES
+.Bl -tag -compact
+.It Pa ~/.unison
+Unison stores a variety of information in a private directory on each host.
+This is the default path of this private directory. This path may be changed by
+the
+.Sy UNISON
+environment variable.
+.Pp
+.It Pa ~/.unison/*.prf
+Profile files. Each profile is stored in a file named
+.Em profilename Ns Sy .prf .
+.Pp
+.It Pa ~/.unison/ar*
+.It Pa ~/.unison/tm*
+.It Pa ~/.unison/sc*
+Main and temporary archive files. These files may be deleted if you know what
+you are doing. Deleting an archive file is equivalent to using the
+.Fl ignorearchives
+option.
+.Pp
+.It Pa ~/.unison/fp*
+Fingerprint cache files. These files may be safely deleted. Keep in mind that
+deleting a fingerprint cache file means that any unsynchronized changes must be
+scanned again. Depending on your replicas, this may mean scanning gigabytes of
+file contents.
+.Pp
+.It Pa ~/.unison/lk*
+Lock files indicating a running Unison process. These files may be deleted if
+you are careful and know that there is no Unison process currently running.
+Deleting a lock file is equivalent to using the
+.Fl ignorelocks
+option.
+.El
+.Sh EXAMPLES
+.Bl -tag -width ""
+.It Sy Synchronize two local directories
+.Pp
+.Dl unison path/to/dir1 /dir2
+.Pp
+This command synchronizes two local directories using the default options.
+Default options are defined by Unison and can be overridden by user in a
+profile called
+.Dq default ,
+which is by default stored in file
+.Pa ~/.unison/default.prf
+.It Sy Synchronize a local and a remote directory
+.Pp
+.Dl unison local/dir ssh://user@host//absolute/path
+.Pp
+This command synchronizes a local directory (here specified by a relative path)
+and a remote directory (here specified by an absolute path) using
+.Xr ssh 1
+and the default options (see example above).
+.It Sy Synchronize with all options specified in a profile
+.Pp
+.Dl unison profilename
+.Pp
+This command reads all the options from the profile named
+.Dq profilename
+and synchronizes according to those options.
+.It Sy Synchronize with options specified in a profile and roots on command line
+.Pp
+.Dl unison profilename /path/to/dir ssh://host/path/on/server
+.Pp
+This command reads all options from the profile named
+.Dq profilename
+with only the roots specified on the command line. Roots must not be specified
+in the profile as the roots from command line will not override roots in the
+profile, rather append to the list of roots.
+.It Sy Synchronize automatically
+.Pp
+.Dl unison -batch /path/to/dir ssh://host/path/on/server
+.Pp
+This command synchronizes all non-conflicting changes automatically, once.
+.It Sy Synchronize continuously
+.Pp
+.Dl unison -repeat watch /path/to/dir ssh://host/path/on/server
+.Pp
+This command first fully synchronizes the roots and then remains dormant,
+waiting for any file changes within either root and then automatically
+synchronizes these changes. This also works in a profile
+.No ( Ns Sy "repeat = watch" ) .
+If the filesystem monitoring helper program is not available or not desired for
+other reasons, it is possible to make Unison synchronize repeatedly with a
+defined time interval:
+.Pp
+.Dl unison -repeat 60 /path/to/dir ssh://host/path/on/server
+.Pp
+This command synchronizes every 60 seconds. Using
+.Fl repeat
+implies
+.Fl batch .
+.Pp
+Currently, continuous synchronization is not possible when using the GUI.
+.El
+.Sh DIAGNOSTICS
+When running in the textual mode, Unison returns an exit status, which
+describes whether, and at which level, the synchronization was successful. The
+exit status could be useful when Unison is invoked from a script. Currently,
+there are four possible values for the exit status:
+.Bl -tag -width 1m
+.It 0
+successful synchronization; everything is up-to-date now.
+.It 1
+some files were skipped, but all file transfers were successful.
+.It 2
+non-fatal failures occurred during file transfer.
+.It 3
+a fatal error occurred, or the execution was interrupted.
+.El
+.Pp
+The graphical interface does not return any useful information through the exit
+status.
+.Sh COMPATIBILITY
+If you are using Unison versions \*(>= 2.52 on all machines, you do not have to
+do anything extra for compatibility.
+.Pp
+Historically (versions \*(Lt 2.52), Unison versions had to be matched
+relatively exactly for them to work together. Additionally, the version of
+compiler used to build Unison also had significant relevance for compatibility.
+.Pp
+As of version 2.52, Unison has a degree of backward and forward compatibility.
+This means three things. First, it is possible for local and remote machines to
+run a different version of Unison. Second, it is possible for local and remote
+machines to run a version (same or different) of Unison built with a different
+version of compiler. Lastly, it is possible to upgrade Unison on the local
+machine and keep the existing archive.
+.Pp
+For more information on co-existence of versions \*(Lt 2.52 and \*(>= 2.52, see
+.Lk https://github.com/bcpierce00/unison/wiki/2.52-Migration-Guide
+.Sh SEE ALSO
+There is a full user manual (pdf, html and txt) included with Unison and
+available online. Depending on your operating system, this manual may have been
+installed at
+.Pa /usr/share/doc/unison/
+or a similar location. The manual can also be read in the GUI (look in the Help
+menu) or on the command line by
+.Sy unison -doc all
+(you probably want to pipe the output to a pager).
+.Pp
+.Lk https://github.com/bcpierce00/unison
+.Pp
+.Lk https://www.cis.upenn.edu/~bcpierce/unison/
+.\" .Sh STANDARDS
+.\" .Sh HISTORY
+.\" .Sh BUGS
diff --git a/setup/README.md b/setup/README.md
deleted file mode 100644
index 226bd28..0000000
--- a/setup/README.md
+++ /dev/null
@@ -1,9 +0,0 @@
-This directory contains the files for creating setups for Microsoft Windows using [Inno Setup](https://github.com/jrsoftware/issrc).
-
-Building Unison setup
----------------------
-
-* Download GTK 2 runtime and place it in directory `bin`
-* Download Unison binaries from https://www.irif.fr/~vouillon/unison/ and place Unison GTK binary together with `unison-fsmonitor.exe` in directory `bin`
-* Download `plink.exe` and `putty.exe` from https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html and place both in directory `bin`
-* Open `Unison.iss` by Inno Setup, adjust version information and compile it
diff --git a/setup/Unison.iss b/setup/Unison.iss
deleted file mode 100644
index 061d681..0000000
--- a/setup/Unison.iss
+++ /dev/null
@@ -1,50 +0,0 @@
-#define MyAppName "Unison"
-#define MyAppURL "http://www.cis.upenn.edu/~bcpierce/unison/"
-#define MyAppExeName "unison 2.48.4 GTK.exe"
-#define ProductVersion "2.48.4"
-
-[Setup]
-AppId={{2B41C63F-C8D0-411C-8BB5-8AACD2BA33ED}
-AppName={#MyAppName}
-AppVersion={#ProductVersion}
-AppVerName={#MyAppName} {#ProductVersion}
-AppCopyright=Benjamin Pierce
-AppPublisher=Benjamin Pierce
-AppPublisherURL={#MyAppURL}
-AppSupportURL={#MyAppURL}/lists.html
-CreateAppDir=yes
-DefaultDirName={pf}\{#MyAppName}
-DefaultGroupName={#MyAppName}
-DisableDirPage=auto
-DisableProgramGroupPage=yes
-LicenseFile=../LICENSE
-InfoAfterFile=postinstall.txt
-OutputDir=.
-OutputBaseFilename=unison{#ProductVersion}_setup
-Compression=lzma
-SolidCompression=yes
-UninstallDisplayIcon={app}\{#MyAppExeName}
-VersionInfoVersion={#ProductVersion}
-
-[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"
-Name: "french"; MessagesFile: "compiler:Languages\French.isl"
-Name: "german"; MessagesFile: "compiler:Languages\German.isl"
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
-
-[Files]
-Source: "plink.bat"; DestDir: "{app}"; Flags: ignoreversion
-Source: "..\bin\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
-
-[Icons]
-Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
-Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: postinstall shellexec
-
-[Messages]
-BeveledLabel=Inno Setup
diff --git a/setup/plink.bat b/setup/plink.bat
deleted file mode 100644
index 3c159bb..0000000
--- a/setup/plink.bat
+++ /dev/null
@@ -1 +0,0 @@
-@plink %1 %2 %3 -v -load Unison "unison -server -contactquietly"
\ No newline at end of file
diff --git a/setup/postinstall.txt b/setup/postinstall.txt
deleted file mode 100644
index 84d307b..0000000
--- a/setup/postinstall.txt
+++ /dev/null
@@ -1 +0,0 @@
-Create a SSH configuration named "Unison" using Putty from the installation directory or edit "plink.bat" file and replace "Unison" by an existing configuration name.
\ No newline at end of file
diff --git a/src/.depend b/src/.depend
index 432a1f4..dc358a3 100644
--- a/src/.depend
+++ b/src/.depend
@@ -13,10 +13,13 @@ abort.cmx : \
 abort.cmi : \
     uutil.cmi
 bytearray.cmo : \
+    ubase/umarshal.cmi \
     bytearray.cmi
 bytearray.cmx : \
+    ubase/umarshal.cmx \
     bytearray.cmi
-bytearray.cmi :
+bytearray.cmi : \
+    ubase/umarshal.cmi
 case.cmo : \
     uutil.cmi \
     ubase/util.cmi \
@@ -50,6 +53,7 @@ clroot.cmi :
 common.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/safelist.cmi \
     props.cmi \
     path.cmi \
@@ -62,6 +66,7 @@ common.cmo : \
 common.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/safelist.cmx \
     props.cmx \
     path.cmx \
@@ -73,6 +78,7 @@ common.cmx : \
     common.cmi
 common.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     props.cmi \
     path.cmi \
     osx.cmi \
@@ -80,22 +86,12 @@ common.cmi : \
     name.cmi \
     fspath.cmi \
     fileinfo.cmi
-compat402.cmo :
-compat402.cmx :
-compat403.cmo :
-compat403.cmx :
-compat408.cmo : \
-    compat408.cmi
-compat408.cmx : \
-    compat408.cmi
-compat408.cmi :
-configure.cmo :
-configure.cmx :
 copy.cmo : \
     xferhint.cmi \
     uutil.cmi \
     ubase/util.cmi \
     update.cmi \
+    ubase/umarshal.cmi \
     transfer.cmi \
     ubase/trace.cmi \
     ubase/safelist.cmi \
@@ -124,6 +120,7 @@ copy.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
+    ubase/umarshal.cmx \
     transfer.cmx \
     ubase/trace.cmx \
     ubase/safelist.cmx \
@@ -159,24 +156,36 @@ copy.cmi : \
     common.cmi
 external.cmo : \
     ubase/util.cmi \
+    terminal.cmi \
     system.cmi \
     ubase/safelist.cmi \
+    remote.cmi \
     lwt/lwt_util.cmi \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     external.cmi
 external.cmx : \
     ubase/util.cmx \
+    terminal.cmx \
     system.cmx \
     ubase/safelist.cmx \
+    remote.cmx \
     lwt/lwt_util.cmx \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     external.cmi
 external.cmi : \
     lwt/lwt.cmi
+features.cmo : \
+    ubase/util.cmi \
+    features.cmi
+features.cmx : \
+    ubase/util.cmx \
+    features.cmi
+features.cmi :
 fileinfo.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     system.cmi \
     props.cmi \
     ubase/prefs.cmi \
@@ -188,6 +197,7 @@ fileinfo.cmo : \
     fileinfo.cmi
 fileinfo.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     system.cmx \
     props.cmx \
     ubase/prefs.cmx \
@@ -198,7 +208,7 @@ fileinfo.cmx : \
     fs.cmx \
     fileinfo.cmi
 fileinfo.cmi : \
-    system.cmi \
+    ubase/umarshal.cmi \
     props.cmi \
     ubase/prefs.cmi \
     path.cmi \
@@ -209,6 +219,7 @@ files.cmo : \
     uutil.cmi \
     ubase/util.cmi \
     update.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     system.cmi \
     stasher.cmi \
@@ -239,6 +250,7 @@ files.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     system.cmx \
     stasher.cmx \
@@ -266,7 +278,6 @@ files.cmx : \
     files.cmi
 files.cmi : \
     uutil.cmi \
-    system.cmi \
     props.cmi \
     path.cmi \
     lwt/lwt_util.cmi \
@@ -280,6 +291,7 @@ fileutil.cmi :
 fingerprint.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     path.cmi \
     fspath.cmi \
     fs.cmi \
@@ -287,17 +299,20 @@ fingerprint.cmo : \
 fingerprint.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     path.cmx \
     fspath.cmx \
     fs.cmx \
     fingerprint.cmi
 fingerprint.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     path.cmi \
     fspath.cmi
 fpcache.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     system.cmi \
     ubase/safelist.cmi \
@@ -312,6 +327,7 @@ fpcache.cmo : \
 fpcache.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     system.cmx \
     ubase/safelist.cmx \
@@ -324,7 +340,6 @@ fpcache.cmx : \
     fileinfo.cmx \
     fpcache.cmi
 fpcache.cmi : \
-    system.cmi \
     props.cmi \
     path.cmi \
     osx.cmi \
@@ -332,9 +347,11 @@ fpcache.cmi : \
     fspath.cmi \
     fileinfo.cmi
 fs.cmo : \
+    system.cmi \
     fspath.cmi \
     fs.cmi
 fs.cmx : \
+    system.cmx \
     fspath.cmx \
     fs.cmi
 fs.cmi : \
@@ -398,6 +415,7 @@ fsmonitor/windows/watcher.cmx : \
 fspath.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     system.cmi \
     ubase/rx.cmi \
     path.cmi \
@@ -407,6 +425,7 @@ fspath.cmo : \
 fspath.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     system.cmx \
     ubase/rx.cmx \
     path.cmx \
@@ -414,13 +433,14 @@ fspath.cmx : \
     fileutil.cmx \
     fspath.cmi
 fspath.cmi : \
-    system.cmi \
+    ubase/umarshal.cmi \
     path.cmi \
     name.cmi
 fswatch.cmo : \
     uutil.cmi \
     ubase/util.cmi \
     ubase/trace.cmi \
+    terminal.cmi \
     system.cmi \
     ubase/prefs.cmi \
     path.cmi \
@@ -432,6 +452,7 @@ fswatch.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     ubase/trace.cmx \
+    terminal.cmx \
     system.cmx \
     ubase/prefs.cmx \
     path.cmx \
@@ -478,6 +499,7 @@ fswatchold.cmi : \
     fspath.cmi
 globals.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     ubase/safelist.cmi \
     remote.cmi \
@@ -485,6 +507,7 @@ globals.cmo : \
     pred.cmi \
     path.cmi \
     os.cmi \
+    negotiate.cmi \
     name.cmi \
     lwt/lwt_util.cmi \
     lwt/lwt_unix.cmi \
@@ -494,6 +517,7 @@ globals.cmo : \
     globals.cmi
 globals.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     ubase/safelist.cmx \
     remote.cmx \
@@ -501,6 +525,7 @@ globals.cmx : \
     pred.cmx \
     path.cmx \
     os.cmx \
+    negotiate.cmx \
     name.cmx \
     lwt/lwt_util.cmx \
     lwt/lwt_unix.cmx \
@@ -509,20 +534,18 @@ globals.cmx : \
     clroot.cmx \
     globals.cmi
 globals.cmi : \
+    terminal.cmi \
     ubase/prefs.cmi \
     pred.cmi \
     path.cmi \
     lwt/lwt.cmi \
-    common.cmi
-linkgtk.cmo : \
-    main.cmo
-linkgtk.cmx : \
-    main.cmx
-linkgtk2.cmo : \
-    uigtk2.cmi \
+    common.cmi \
+    clroot.cmi
+linkgtk3.cmo : \
+    uigtk3.cmi \
     main.cmo
-linkgtk2.cmx : \
-    uigtk2.cmx \
+linkgtk3.cmx : \
+    uigtk3.cmx \
     main.cmx
 linktext.cmo : \
     uitext.cmi \
@@ -538,18 +561,7 @@ lock.cmx : \
     ubase/util.cmx \
     system.cmx \
     lock.cmi
-lock.cmi : \
-    system.cmi
-lwt/example/editor.cmo : \
-    lwt/lwt_unix.cmi
-lwt/example/editor.cmx : \
-    lwt/lwt_unix.cmx
-lwt/example/relay.cmo : \
-    lwt/lwt_unix.cmi \
-    lwt/lwt.cmi
-lwt/example/relay.cmx : \
-    lwt/lwt_unix.cmx \
-    lwt/lwt.cmx
+lock.cmi :
 lwt/generic/lwt_unix_impl.cmo : \
     lwt/pqueue.cmi \
     lwt/lwt.cmi
@@ -581,9 +593,11 @@ lwt/pqueue.cmx : \
     lwt/pqueue.cmi
 lwt/pqueue.cmi :
 lwt/win/lwt_unix_impl.cmo : \
+    system/system_win.cmo \
     lwt/pqueue.cmi \
     lwt/lwt.cmi
 lwt/win/lwt_unix_impl.cmx : \
+    system/system_win.cmx \
     lwt/pqueue.cmx \
     lwt/lwt.cmx
 lwt/win/lwt_win.cmo : \
@@ -597,6 +611,7 @@ main.cmo : \
     ubase/util.cmi \
     uitext.cmi \
     uicommon.cmi \
+    system.cmi \
     strings.cmi \
     ubase/safelist.cmi \
     remote.cmi \
@@ -607,6 +622,7 @@ main.cmx : \
     ubase/util.cmx \
     uitext.cmx \
     uicommon.cmx \
+    system.cmx \
     strings.cmx \
     ubase/safelist.cmx \
     remote.cmx \
@@ -614,17 +630,42 @@ main.cmx : \
     os.cmx
 name.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/rx.cmi \
     case.cmi \
     name.cmi
 name.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/rx.cmx \
     case.cmx \
     name.cmi
-name.cmi :
+name.cmi : \
+    ubase/umarshal.cmi
+negotiate.cmo : \
+    ubase/util.cmi \
+    ubase/umarshal.cmi \
+    ubase/safelist.cmi \
+    remote.cmi \
+    lwt/lwt.cmi \
+    features.cmi \
+    common.cmi \
+    negotiate.cmi
+negotiate.cmx : \
+    ubase/util.cmx \
+    ubase/umarshal.cmx \
+    ubase/safelist.cmx \
+    remote.cmx \
+    lwt/lwt.cmx \
+    features.cmx \
+    common.cmx \
+    negotiate.cmi
+negotiate.cmi : \
+    lwt/lwt.cmi \
+    common.cmi
 os.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     system.cmi \
     ubase/safelist.cmi \
@@ -641,6 +682,7 @@ os.cmo : \
     os.cmi
 os.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     system.cmx \
     ubase/safelist.cmx \
@@ -657,7 +699,7 @@ os.cmx : \
     os.cmi
 os.cmi : \
     uutil.cmi \
-    props.cmi \
+    ubase/umarshal.cmi \
     path.cmi \
     name.cmi \
     fspath.cmi \
@@ -665,8 +707,8 @@ os.cmi : \
 osx.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
-    system.cmi \
     ubase/safelist.cmi \
     ubase/prefs.cmi \
     path.cmi \
@@ -678,8 +720,8 @@ osx.cmo : \
 osx.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
-    system.cmx \
     ubase/safelist.cmx \
     ubase/prefs.cmx \
     path.cmx \
@@ -690,12 +732,14 @@ osx.cmx : \
     osx.cmi
 osx.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     ubase/prefs.cmi \
     path.cmi \
     fspath.cmi \
     fingerprint.cmi
 path.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/safelist.cmi \
     ubase/rx.cmi \
     pred.cmi \
@@ -705,6 +749,7 @@ path.cmo : \
     path.cmi
 path.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/safelist.cmx \
     ubase/rx.cmx \
     pred.cmx \
@@ -713,12 +758,14 @@ path.cmx : \
     case.cmx \
     path.cmi
 path.cmi : \
+    ubase/umarshal.cmi \
     pred.cmi \
     name.cmi
 pixmaps.cmo :
 pixmaps.cmx :
 pred.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/safelist.cmi \
     ubase/rx.cmi \
     ubase/prefs.cmi \
@@ -726,40 +773,61 @@ pred.cmo : \
     pred.cmi
 pred.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/safelist.cmx \
     ubase/rx.cmx \
     ubase/prefs.cmx \
     case.cmx \
     pred.cmi
-pred.cmi :
+pred.cmi : \
+    ubase/prefs.cmi
 props.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
+    system.cmi \
+    ubase/safelist.cmi \
+    propsdata.cmi \
     ubase/prefs.cmi \
+    pred.cmi \
     path.cmi \
     osx.cmi \
-    lwt/lwt_unix.cmi \
     fspath.cmi \
     fs.cmi \
-    external.cmi \
+    features.cmi \
     props.cmi
 props.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
+    system.cmx \
+    ubase/safelist.cmx \
+    propsdata.cmx \
     ubase/prefs.cmx \
+    pred.cmx \
     path.cmx \
     osx.cmx \
-    lwt/lwt_unix.cmx \
     fspath.cmx \
     fs.cmx \
-    external.cmx \
+    features.cmx \
     props.cmi
 props.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     ubase/prefs.cmi \
+    pred.cmi \
     path.cmi \
     osx.cmi \
     fspath.cmi
+propsdata.cmo : \
+    ubase/util.cmi \
+    ubase/safelist.cmi \
+    propsdata.cmi
+propsdata.cmx : \
+    ubase/util.cmx \
+    ubase/safelist.cmx \
+    propsdata.cmi
+propsdata.cmi :
 recon.cmo : \
     uutil.cmi \
     ubase/util.cmi \
@@ -776,6 +844,7 @@ recon.cmo : \
     globals.cmi \
     fileinfo.cmi \
     common.cmi \
+    clroot.cmi \
     recon.cmi
 recon.cmx : \
     uutil.cmx \
@@ -793,6 +862,7 @@ recon.cmx : \
     globals.cmx \
     fileinfo.cmx \
     common.cmx \
+    clroot.cmx \
     recon.cmi
 recon.cmi : \
     props.cmi \
@@ -801,6 +871,7 @@ recon.cmi : \
 remote.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     terminal.cmi \
     system.cmi \
@@ -811,15 +882,14 @@ remote.cmo : \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     fspath.cmi \
-    fs.cmi \
     common.cmi \
     clroot.cmi \
-    case.cmi \
     bytearray.cmi \
     remote.cmi
 remote.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     terminal.cmx \
     system.cmx \
@@ -830,14 +900,15 @@ remote.cmx : \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     fspath.cmx \
-    fs.cmx \
     common.cmx \
     clroot.cmx \
-    case.cmx \
     bytearray.cmx \
     remote.cmi
 remote.cmi : \
+    ubase/umarshal.cmi \
+    terminal.cmi \
     ubase/prefs.cmi \
+    lwt/lwt_util.cmi \
     lwt/lwt.cmi \
     fspath.cmi \
     common.cmi \
@@ -865,6 +936,7 @@ stasher.cmo : \
     xferhint.cmi \
     ubase/util.cmi \
     update.cmi \
+    ubase/umarshal.cmi \
     system.cmi \
     ubase/safelist.cmi \
     remote.cmi \
@@ -888,6 +960,7 @@ stasher.cmx : \
     xferhint.cmx \
     ubase/util.cmx \
     update.cmx \
+    ubase/umarshal.cmx \
     system.cmx \
     ubase/safelist.cmx \
     remote.cmx \
@@ -928,42 +1001,46 @@ system/generic/system_impl.cmo : \
     system/system_generic.cmo
 system/generic/system_impl.cmx : \
     system/system_generic.cmx
-system/system_generic.cmo :
-system/system_generic.cmx :
-system/system_intf.cmo :
-system/system_intf.cmx :
+system/system_generic.cmo : \
+    ubase/umarshal.cmi
+system/system_generic.cmx : \
+    ubase/umarshal.cmx
+system/system_intf.cmo : \
+    ubase/umarshal.cmi
+system/system_intf.cmx : \
+    ubase/umarshal.cmx
 system/system_win.cmo : \
-    unicode.cmi \
     system/system_generic.cmo \
     ubase/rx.cmi
 system/system_win.cmx : \
-    unicode.cmx \
     system/system_generic.cmx \
     ubase/rx.cmx
 system/win/system_impl.cmo : \
-    system/system_win.cmo \
-    system/system_generic.cmo
+    system/system_win.cmo
 system/win/system_impl.cmx : \
-    system/system_win.cmx \
-    system/system_generic.cmx
+    system/system_win.cmx
 terminal.cmo : \
+    ubase/util.cmi \
     system.cmi \
     ubase/rx.cmi \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     terminal.cmi
 terminal.cmx : \
+    ubase/util.cmx \
     system.cmx \
     ubase/rx.cmx \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     terminal.cmi
 terminal.cmi : \
-    lwt/lwt_unix.cmi
+    lwt/lwt_unix.cmi \
+    lwt/lwt.cmi
 test.cmo : \
     uutil.cmi \
     ubase/util.cmi \
     update.cmi \
+    ubase/umarshal.cmi \
     uicommon.cmi \
     transport.cmi \
     ubase/trace.cmi \
@@ -987,6 +1064,7 @@ test.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
+    ubase/umarshal.cmx \
     uicommon.cmx \
     transport.cmx \
     ubase/trace.cmx \
@@ -1010,6 +1088,7 @@ test.cmi :
 transfer.cmo : \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     ubase/safelist.cmi \
     lwt/lwt.cmi \
@@ -1019,6 +1098,7 @@ transfer.cmo : \
 transfer.cmx : \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     ubase/safelist.cmx \
     lwt/lwt.cmx \
@@ -1027,6 +1107,7 @@ transfer.cmx : \
     transfer.cmi
 transfer.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     lwt/lwt.cmi \
     bytearray.cmi
 transport.cmo : \
@@ -1066,41 +1147,52 @@ transport.cmi : \
     lwt/lwt.cmi \
     common.cmi
 tree.cmo : \
+    ubase/umarshal.cmi \
     ubase/safelist.cmi \
     tree.cmi
 tree.cmx : \
+    ubase/umarshal.cmx \
     ubase/safelist.cmx \
     tree.cmi
-tree.cmi :
+tree.cmi : \
+    ubase/umarshal.cmi
 ubase/myMap.cmo : \
+    ubase/umarshal.cmi \
     ubase/myMap.cmi
 ubase/myMap.cmx : \
+    ubase/umarshal.cmx \
     ubase/myMap.cmi
-ubase/myMap.cmi :
+ubase/myMap.cmi : \
+    ubase/umarshal.cmi
 ubase/prefs.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/uarg.cmi \
     system.cmi \
     ubase/safelist.cmi \
     ubase/prefs.cmi
 ubase/prefs.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/uarg.cmx \
     system.cmx \
     ubase/safelist.cmx \
     ubase/prefs.cmi
 ubase/prefs.cmi : \
     ubase/util.cmi \
-    system.cmi
+    ubase/umarshal.cmi
 ubase/projectInfo.cmo :
 ubase/projectInfo.cmx :
 ubase/proplist.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/proplist.cmi
 ubase/proplist.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/proplist.cmi
-ubase/proplist.cmi :
+ubase/proplist.cmi : \
+    ubase/umarshal.cmi
 ubase/rx.cmo : \
     ubase/rx.cmi
 ubase/rx.cmx : \
@@ -1113,17 +1205,20 @@ ubase/safelist.cmx : \
 ubase/safelist.cmi :
 ubase/trace.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     system.cmi \
     ubase/safelist.cmi \
     ubase/prefs.cmi \
     ubase/trace.cmi
 ubase/trace.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     system.cmx \
     ubase/safelist.cmx \
     ubase/prefs.cmx \
     ubase/trace.cmi
 ubase/trace.cmi : \
+    ubase/umarshal.cmi \
     ubase/prefs.cmi
 ubase/uarg.cmo : \
     ubase/util.cmi \
@@ -1136,6 +1231,11 @@ ubase/uarg.cmx : \
     ubase/safelist.cmx \
     ubase/uarg.cmi
 ubase/uarg.cmi :
+ubase/umarshal.cmo : \
+    ubase/umarshal.cmi
+ubase/umarshal.cmx : \
+    ubase/umarshal.cmi
+ubase/umarshal.cmi :
 ubase/util.cmo : \
     system.cmi \
     ubase/safelist.cmi \
@@ -1146,14 +1246,15 @@ ubase/util.cmx : \
     ubase/safelist.cmx \
     ubase/projectInfo.cmx \
     ubase/util.cmi
-ubase/util.cmi : \
-    system.cmi
+ubase/util.cmi :
 ui.cmi :
 uicommon.cmo : \
     xferhint.cmi \
     uutil.cmi \
     ubase/util.cmi \
     update.cmi \
+    ubase/umarshal.cmi \
+    transport.cmi \
     ubase/trace.cmi \
     system.cmi \
     stasher.cmi \
@@ -1174,15 +1275,19 @@ uicommon.cmo : \
     fspath.cmi \
     files.cmi \
     fileinfo.cmi \
+    features.cmi \
     common.cmi \
     clroot.cmi \
     case.cmi \
+    abort.cmi \
     uicommon.cmi
 uicommon.cmx : \
     xferhint.cmx \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
+    ubase/umarshal.cmx \
+    transport.cmx \
     ubase/trace.cmx \
     system.cmx \
     stasher.cmx \
@@ -1203,17 +1308,21 @@ uicommon.cmx : \
     fspath.cmx \
     files.cmx \
     fileinfo.cmx \
+    features.cmx \
     common.cmx \
     clroot.cmx \
     case.cmx \
+    abort.cmx \
     uicommon.cmi
 uicommon.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
+    terminal.cmi \
     ubase/prefs.cmi \
     path.cmi \
     lwt/lwt.cmi \
     common.cmi
-uigtk2.cmo : \
+uigtk3.cmo : \
     uutil.cmi \
     ubase/util.cmi \
     update.cmi \
@@ -1222,6 +1331,7 @@ uigtk2.cmo : \
     uicommon.cmi \
     transport.cmi \
     ubase/trace.cmi \
+    terminal.cmi \
     system.cmi \
     strings.cmi \
     sortri.cmi \
@@ -1231,16 +1341,15 @@ uigtk2.cmo : \
     ubase/prefs.cmi \
     pixmaps.cmo \
     path.cmi \
-    os.cmi \
-    lwt/lwt_util.cmi \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     globals.cmi \
     common.cmi \
     clroot.cmi \
     case.cmi \
-    uigtk2.cmi
-uigtk2.cmx : \
+    abort.cmi \
+    uigtk3.cmi
+uigtk3.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
@@ -1249,6 +1358,7 @@ uigtk2.cmx : \
     uicommon.cmx \
     transport.cmx \
     ubase/trace.cmx \
+    terminal.cmx \
     system.cmx \
     strings.cmx \
     sortri.cmx \
@@ -1258,70 +1368,17 @@ uigtk2.cmx : \
     ubase/prefs.cmx \
     pixmaps.cmx \
     path.cmx \
-    os.cmx \
-    lwt/lwt_util.cmx \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     globals.cmx \
     common.cmx \
     clroot.cmx \
     case.cmx \
-    uigtk2.cmi
-uigtk2.cmi : \
+    abort.cmx \
+    uigtk3.cmi
+uigtk3.cmi : \
     uicommon.cmi
 uimacbridge.cmo : \
-    xferhint.cmi \
-    uutil.cmi \
-    ubase/util.cmi \
-    update.cmi \
-    uicommon.cmi \
-    transport.cmi \
-    ubase/trace.cmi \
-    terminal.cmi \
-    system.cmi \
-    stasher.cmi \
-    ubase/safelist.cmi \
-    remote.cmi \
-    recon.cmi \
-    ubase/prefs.cmi \
-    path.cmi \
-    os.cmi \
-    main.cmo \
-    lwt/lwt_util.cmi \
-    lwt/lwt_unix.cmi \
-    lwt/lwt.cmi \
-    globals.cmi \
-    fspath.cmi \
-    files.cmi \
-    common.cmi \
-    clroot.cmi
-uimacbridge.cmx : \
-    xferhint.cmx \
-    uutil.cmx \
-    ubase/util.cmx \
-    update.cmx \
-    uicommon.cmx \
-    transport.cmx \
-    ubase/trace.cmx \
-    terminal.cmx \
-    system.cmx \
-    stasher.cmx \
-    ubase/safelist.cmx \
-    remote.cmx \
-    recon.cmx \
-    ubase/prefs.cmx \
-    path.cmx \
-    os.cmx \
-    main.cmx \
-    lwt/lwt_util.cmx \
-    lwt/lwt_unix.cmx \
-    lwt/lwt.cmx \
-    globals.cmx \
-    fspath.cmx \
-    files.cmx \
-    common.cmx \
-    clroot.cmx
-uimacbridgenew.cmo : \
     xferhint.cmi \
     uutil.cmi \
     ubase/util.cmi \
@@ -1340,7 +1397,6 @@ uimacbridgenew.cmo : \
     path.cmi \
     os.cmi \
     main.cmo \
-    lwt/lwt_util.cmi \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     globals.cmi \
@@ -1348,7 +1404,7 @@ uimacbridgenew.cmo : \
     files.cmi \
     common.cmi \
     clroot.cmi
-uimacbridgenew.cmx : \
+uimacbridge.cmx : \
     xferhint.cmx \
     uutil.cmx \
     ubase/util.cmx \
@@ -1367,7 +1423,6 @@ uimacbridgenew.cmx : \
     path.cmx \
     os.cmx \
     main.cmx \
-    lwt/lwt_util.cmx \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     globals.cmx \
@@ -1380,6 +1435,7 @@ uitext.cmo : \
     ubase/util.cmi \
     update.cmi \
     unicode.cmi \
+    ubase/umarshal.cmi \
     uicommon.cmi \
     transport.cmi \
     ubase/trace.cmi \
@@ -1390,18 +1446,20 @@ uitext.cmo : \
     recon.cmi \
     ubase/prefs.cmi \
     path.cmi \
-    lwt/lwt_util.cmi \
     lwt/lwt_unix.cmi \
     lwt/lwt.cmi \
     globals.cmi \
     fswatchold.cmi \
+    fspath.cmi \
     common.cmi \
+    abort.cmi \
     uitext.cmi
 uitext.cmx : \
     uutil.cmx \
     ubase/util.cmx \
     update.cmx \
     unicode.cmx \
+    ubase/umarshal.cmx \
     uicommon.cmx \
     transport.cmx \
     ubase/trace.cmx \
@@ -1412,12 +1470,13 @@ uitext.cmx : \
     recon.cmx \
     ubase/prefs.cmx \
     path.cmx \
-    lwt/lwt_util.cmx \
     lwt/lwt_unix.cmx \
     lwt/lwt.cmx \
     globals.cmx \
     fswatchold.cmx \
+    fspath.cmx \
     common.cmx \
+    abort.cmx \
     uitext.cmi
 uitext.cmi : \
     uicommon.cmi
@@ -1434,6 +1493,7 @@ update.cmo : \
     xferhint.cmi \
     uutil.cmi \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     tree.cmi \
     ubase/trace.cmi \
     system.cmi \
@@ -1458,6 +1518,7 @@ update.cmo : \
     fpcache.cmi \
     fingerprint.cmi \
     fileinfo.cmi \
+    features.cmi \
     common.cmi \
     case.cmi \
     update.cmi
@@ -1465,6 +1526,7 @@ update.cmx : \
     xferhint.cmx \
     uutil.cmx \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     tree.cmx \
     ubase/trace.cmx \
     system.cmx \
@@ -1489,11 +1551,13 @@ update.cmx : \
     fpcache.cmx \
     fingerprint.cmx \
     fileinfo.cmx \
+    features.cmx \
     common.cmx \
     case.cmx \
     update.cmi
 update.cmi : \
     uutil.cmi \
+    ubase/umarshal.cmi \
     tree.cmi \
     props.cmi \
     path.cmi \
@@ -1507,15 +1571,18 @@ update.cmi : \
     common.cmi
 uutil.cmo : \
     ubase/util.cmi \
+    ubase/umarshal.cmi \
     ubase/trace.cmi \
     ubase/projectInfo.cmo \
     uutil.cmi
 uutil.cmx : \
     ubase/util.cmx \
+    ubase/umarshal.cmx \
     ubase/trace.cmx \
     ubase/projectInfo.cmx \
     uutil.cmi
-uutil.cmi :
+uutil.cmi : \
+    ubase/umarshal.cmi
 xferhint.cmo : \
     ubase/util.cmi \
     ubase/trace.cmi \
diff --git a/src/FEATURES.md b/src/FEATURES.md
new file mode 100644
index 0000000..67f7ed5
--- /dev/null
+++ b/src/FEATURES.md
@@ -0,0 +1,185 @@
+# Introduction and motivation
+
+"Features" is a set of feature names supported by a specific version of
+Unison implementation. Over time, each incompatible change -- whether
+mandatory or an optional add-on -- is assigned a unique feature name.
+
+Features allow a client to connect to and properly work with a server of
+different version, older or newer. When setting up the connection, both
+server and client negotiate a commonly supported set of features.
+
+Using features instead of a version makes the implementation agnostic of any
+versioning schemes, forks and third party implementations. It also allows
+for more flexible code changes over time, without the code being polluted by
+adding more and more conditionals for various version combinations, such as
+"if version < X then", "if version >= Y and version < Z then", and so on.
+
+# Negotiation
+
+Feature negotiation takes place immediately after the RPC connection has
+been fully set up. See `negotiate.ml`.
+
+1. Client sends its full feature set to the server.
+2. Server validates the intersection of its and client's feature sets.
+   - If error then server sends NOK to client. The client closes connection.
+3. If OK then server sends intersection of feature sets to the client.
+4. Client validates the intersection.
+   - If error then client closes connection.
+5. If OK then the negotiation is complete and both server and client will
+   use only features fully supported by both.
+
+## Feature registration
+
+A feature is added to the set by registering it. This can be done by any
+part of the code that "owns" a feature, similar to how user preferences are
+registered. See `features.mli` and `features.ml`.
+
+Registering a feature requires a unique feature name and an optional
+validation function.
+
+## Feature validation
+
+Each feature can provide a separate validation function. When validating
+the intersection of client's and server's feature sets, validation
+functions for each included feature are run in arbitrary sequence.
+
+A validation function will be able to see the entire intersection and can
+freely decide whether the intersection is ok or not. Examples of possible
+validation scenarios:
+
+- A mandatory feature is not in the intersection
+  - This typically means that counterparty is too old, but could also mean
+    that the counterparty is too new and the feature has been removed.
+- User preference enabled for a feature not in intersection
+  (the preferences have not been sent to the server yet, so this
+  validation is not carried out by the server)
+- A feature depends on another feature not in intersection
+
+Some features in the intersection can conflict with each other. This can
+happen for example when two different implementations of a function are
+both supported but must not be used simultaneously. All such conflicts
+are benign in nature and will not cause feature intersection validation
+to fail. (Since the intersection is a subset of the entire feature set
+then failing a conflict would mean that the set of features is conflicting
+to begin with.)
+
+# Development
+
+Every incompatible code change must result in a change to the set of
+features:
+
+- New code that is mandatory to use (effectively breaks compatibility
+  with older versions despite feature negotiation) ->
+  - Register a new feature with a validation function that rejects any
+    feature intersection that does not include this feature
+- New code that is optional to use ->
+  - Register a new feature
+- New code that replaces existing code ->
+  - Register a new feature and remove one or more features
+- Remove existing code ->
+  - Remove one or more features
+- Code is not removed but it can be deprecated ->
+  - Add or change a validation function to output a deprecation warning
+
+## User preferences
+
+User preferences are sent from client to server after establishing a
+connection. The server must know all preferences received from the client,
+otherwise the connection fails.
+
+When new preferences are created with a new feature, it is possible (and in
+most cases required) to add a guard function that determines if the
+preference is sent to the server or not. Typically, this guard function
+will take the form `fun () -> Features.enabled somefeature`, meaning that
+the preference is sent to server if and only if 'somefeature' is known by
+the server.
+
+## Code evolution, conflicting features
+
+With features, new code does not have to replace existing code even if
+they seemingly conflict. Both an existing feature and a new feature can
+co-exist. The code must be guarded by checking which features are enabled
+at runtime for each remote connection.
+
+For example:
+
+- Existing code implements feature hash-1.
+- New code implements a new hashing algorithm and adds feature hash-2.
+- Even though two different hashing algorithms must not be used at the
+  same time, both implementations can co-exist as in the following
+  pseudocode example.
+
+```
+function hash
+  if (feature hash-2 enabled) then
+    new algorithm
+  else if (feature hash-1 enabled) then
+    previous algorithm
+  end
+```
+
+- If both server and client support hash-2 then the new implementation
+  will always be used, even if both server and client also support hash-1
+  at the same time.
+- If either server or client does not support hash-2 then the feature
+  intersection will only contain hash-1 and the previous implementation
+  will be used.
+
+Now let's imagine that in addition to hashing algorithm changing with
+the new feature, also the result type changes. This is trickier to implement
+but clearly not impossible.
+
+There are multiple ways of handling parallel implementation of conflicting
+types. These are not the topic of this document, but a few possibilities
+are provided for inspiration:
+
+- Abstract types and type variables
+- Variant types (aka sum types)
+- Extensible variant types
+- First class modules
+- GADTs
+- Classes/objects
+
+### Archive file
+
+Most changes will ultimately result in type changes. This will directly
+impact data encoded in wire format and stored in archive file format.
+
+Data on the wire is transient. As both client and server have agreed on
+a common feature set, they know how to marshal and unmarshal data on the
+wire without any issues.
+
+Data in the archive file is persistent and could have been written while
+a different set of features was agreed upon. There are a couple of ways
+to read and write archive files in this scenario:
+
+- Not even attempt to read an incompatible archive file. The exact used
+  feature set is written into the archive file. As long as both client and
+  server keep negotiating the same feature set, they can read existing
+  archive files. When the negotiated feature set changes (due to upgrades),
+  the previous archive files can be ignored (requires a complete rescan).
+  This may be acceptable, as such upgrades are assumed to be quite rare.
+
+- A subset of the used feature set is written into the archive file. Only
+  features that change the data structures written in the archive file are
+  stored in the file. The reading can work in two ways. Either as a slightly
+  more forgiving variant of the point above, or actually reading and
+  unmarshaling the archive according to the features used to write it --
+  even if not all the same features are included in the currently negotiated
+  feature set. The latter is the currently chosen approach. It does require
+  types and code be tailored for this, the same as with the next point below,
+  but to a lesser degree.
+
+- The archive file on-disk format includes information about the types and
+  structure of the written data (you can think like a DB with a relatively
+  dynamic but still typed schema). The data can be read back selectively,
+  and even converted as necessary (for example, can read a stored int32 into
+  in-memory int64).
+  The selective reading can mean two things. First, the archive was written
+  with a feature that is no longer enabled. The data that was only relevant
+  to that feature is just skipped. Second, the archive was written without
+  a feature that is now enabled. For the newly-enabled feature there is no
+  data in the archive but this does not break reading the file, as long as
+  the new feature can deal with default or "empty" values for its data
+  structures.
+
diff --git a/src/INSTALL b/src/INSTALL
deleted file mode 100644
index 379c2e9..0000000
--- a/src/INSTALL
+++ /dev/null
@@ -1,2 +0,0 @@
-For installation instructions, see the the INSTALLATION section of the
-user manual.
diff --git a/src/INSTALL.gtk2 b/src/INSTALL.gtk2
deleted file mode 100644
index beaefa6..0000000
--- a/src/INSTALL.gtk2
+++ /dev/null
@@ -1,60 +0,0 @@
-[UPDATE 1/2019: The instructions below are rather old, and there may
-be simpler ways now.  For example, on some systems, just doing "opam
-install lablgtk" seems to be enough to get all the support ibraries
-installed.]
-
-We are happy to announce a new version of Unison with a user interface
-based on Gtk 2.2, enabling display of filenames with any locale encoding.
-
-Installation instructions follow:
-
------------------------------
-LINUX (and maybe other Unixes):
-
-In order to use gtk2 with unison,
-
-1) install glib, pango, gtk (version >2.2)
-   from http://www.gtk.org/
-
-2) install lablgtk2 (version >20030423)
-   from http://wwwfun.kurims.kyoto-u.ac.jp/soft/olabl/lablgtk.html
-
-3) install unison (version >2.9.36)
-   from http://www.cis.upenn.edu/~bcpierce/unison/
-
-   Simply type 'make'.
-
-   Makefile will detect the presence of lablgtk2 directory
-   $(OCAMLLIBDIR)/lablgtk2 (such as /usr/local/lib/ocaml/lablgtk2/)
-   and use UISTYLE=gtk2 by default. If absent, it falls back to
-   lablgtk with UISTYLE=gtk, then back to UISTYLE=text.
-
-   You can force the selection by
-        make UISTYLE=gtk2
-     or make UISTYLE=gtk
-     or make UISTYLE=text
-
-4) setup your locale environment properly
-   for example, export LANG=zh_HK.BIG5-HKSCS
-
-   otherwise, you will get
-     Uncaught exception Glib.GError("Invalid byte sequence in conversion input")
-5) enjoy unison with i18n!
-
------------------------------
-OS X:
-
-1) Install gtk2 using fink:
-
-      sudo /sw/bin/fink  install gtk+2
-
-   Then proceed from step 2 above.
-
-In our tests, the linker generates lots of error messages, but appears
-to build a working executable.  Also, we have not yet been able to get
-this build to work with 'STATIC=true'.
-
------------------------------
-WINDOWS:
-
-(Anybody want to contribute instructions??)
diff --git a/src/INSTALL.win32-msvc b/src/INSTALL.win32-msvc
index b7e1a61..7aa3eeb 100644
--- a/src/INSTALL.win32-msvc
+++ b/src/INSTALL.win32-msvc
@@ -218,7 +218,7 @@ above the 'lablgtk' directory.
 
   The way from public Gtk/LablGtk sources to the provided Gtk/LablGtk
 dynamic/static extension has been somehow perilous. We strongly
-recommand using the provided sources and patches as a base for your
+recommend using the provided sources and patches as a base for your
 further enhancements.
 
   To be exhaustive, here are the steps followed to create the provided
diff --git a/src/Makefile b/src/Makefile
index f109b93..b2e72c7 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -1,8 +1,6 @@
-#######################################################################
-# $I1: Unison file synchronizer: src/Makefile $
-# $I2: Last modified by bcpierce on Sun, 22 Aug 2004 22:29:04 -0400 $
-# $I3: Copyright 1999-2020 (see COPYING for details) $
-#######################################################################
+# Unison file synchronizer: src/Makefile
+# Copyright 1999-2022 (see ../LICENSE for terms).
+
 ## User Settings
 
 # Set NATIVE=false if you are not using the native code compiler (ocamlopt)
@@ -24,27 +22,11 @@ NATIVE=true
 ######################################################################
 # Building installation instructions
 
+.PHONY: all
 all:: strings.ml buildexecutable
 
-all:: INSTALL
-
-.PHONY: all clean install doinstall installtext text \
-	setupdemo-old setupdemo modifydemo demo \
-	run runbatch runt rundebug runp runtext runsort runprefer \
-	prefsdocs runtest repeattest \
-	selftest selftestdebug selftestremote testmerge \
-	checkin installremote
-
-.DELETE_ON_ERROR:
-# to avoid problems when something fails to run
-
-INSTALL: $(NAME)$(EXEC_EXT)
-# file isn't made for OS X, so check that it's there first
-	(if [ -f $(NAME) ]; then ./$(NAME) -doc install > INSTALLATION; fi)
-
 ########################################################################
 ## Miscellaneous developer-only switches
-DEBUGGING=true
 PROFILING=false
 STATIC=false
 
@@ -57,164 +39,29 @@ STATIC=false
 include Makefile.OCaml
 
 ######################################################################
-# Installation
-
-INSTALLDIR = $(HOME)/bin/
-
-# This has two names because on OSX the file INSTALL shadows the target 'install'!
-install: doinstall
-
-installtext:
-	$(MAKE) -C .. installtext
-
-text:
-	$(MAKE) -C .. text
-
-# Note that this is not needed (and doesn't work) for the OSX GUI version.
-# Just copy $(UIMACDIR)/build/Default/Unison.app to where you want it.
-doinstall: buildexecutable
-	@if [ ! $(NAME) ]; then \
-	    echo "makefile variable NAME not bound"; \
-	    exit 1 \
-	  ; fi
-	-mv $(INSTALLDIR)/$(NAME)$(EXEC_EXT) /tmp/$(NAME)-$(shell echo $$$$)
-	cp $(NAME)$(EXEC_EXT) $(INSTALLDIR)
-	cp $(NAME)$(EXEC_EXT) $(INSTALLDIR)$(NAME)-$(MAJORVERSION)$(EXEC_EXT)
-
-######################################################################
-# Demo
-
-setupdemo-old: all
-	-mkdir alice.tmp bob.tmp
-	-touch alice.tmp/letter alice.tmp/curriculum
-	-mkdir bob.tmp/curriculum
-	-touch bob.tmp/curriculum/french
-	-touch bob.tmp/curriculum/german
-	-mkdir bob.tmp/good_friends
-	-mkdir bob.tmp/good_friends/addresses
-	-mkdir alice.tmp/good_friends
-	-touch alice.tmp/good_friends/addresses
-	-touch bob.tmp/good_friends/addresses/alice
-	-mkdir alice.tmp/book
-	-mkdir bob.tmp/book
-	echo "first name:alice \n 2234 Chesnut Street \n Philadelphia" \
-	   > bob.tmp/good_friends/addresses/alice
-	echo "ADDRESS 1 : BOB \n firstName : bob \n 2233 Walnut Street" \
-	   > alice.tmp/good_friends/addresses
-	echo "Born in Paris in 1976 ..." > alice.tmp/curriculum
-	echo "Ne a Paris en 1976 ..." > bob.tmp/curriculum/french
-	echo "Geboren in Paris im jahre 1976 ..." > bob.tmp/curriculum/german
-	echo "Dear friend, I received your letter ..."    > alice.tmp/letter
-	echo "And then the big bad wolf" > bob.tmp/book/page3
-	echo "Title : three little pigs" > alice.tmp/book/page1
-	echo "there was upon a time ..." > alice.tmp/book/page2
-
-setupdemo:
-	rm -rf a.tmp b.tmp
-	mkdir a.tmp
-	touch a.tmp/a a.tmp/b a.tmp/c
-	mkdir a.tmp/d
-	touch a.tmp/d/f
-	touch a.tmp/d/g
-	cp -r a.tmp b.tmp
-
-modifydemo:
-	-rm a.tmp/a
-	echo "Hello" > a.tmp/b
-	echo "Hello" > b.tmp/b
-	date > b.tmp/c
-	echo "Hi there" > a.tmp/d/h
-	echo "Hello there" > b.tmp/d/h
-
-demo: all setupdemo
-	@$(MAKE) run
-	@$(MAKE) modifydemo
-	@$(MAKE) run
-
-run: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/x
-	-date > b.tmp/y
-	./$(NAME) default a.tmp b.tmp
-
-runbatch: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/x
-	-date > b.tmp/y
-	./$(NAME) default a.tmp b.tmp -batch
-
-runt: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/x
-	-date > b.tmp/y
-	./$(NAME) default a.tmp b.tmp -timers
-
-rundebug: all
-	-date > a.tmp/x
-	-date > b.tmp/y
-	./$(NAME) a.tmp b.tmp -debug all  -ui text
-
-runp: all
-	-echo cat > a.tmp/cat
-	-echo cat > b.tmp/cat
-	-chmod 765 a.tmp/cat
-	-chmod 700 b.tmp/cat
-	./$(NAME) a.tmp b.tmp
-
-runtext: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/x
-	-date > b.tmp/y
-	./$(NAME) -ui text a.tmp b.tmp
-
-runsort: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/b
-	-date > b.tmp/m
-	-date > b.tmp/z
-	-date > b.tmp/f
-	-date >> b.tmp/f
-	-date > b.tmp/c.$(shell echo $$$$)
-	-date > b.tmp/y.$(shell echo $$$$)
-	./$(NAME) default a.tmp b.tmp -debug sort
-
-runprefer: all
-	-mkdir a.tmp b.tmp
-	-date > a.tmp/b
-	-date > b.tmp/m
-	-date > b.tmp/z
-	-echo Hello > a.tmp/z
-	-date > b.tmp/f
-	-date >> b.tmp/f
-	-date > b.tmp/c.$(shell echo $$$$)
-	-date > b.tmp/y.$(shell echo $$$$)
-	./$(NAME) default a.tmp b.tmp -force b.tmp
-
-prefsdocs: all
-	./$(NAME) -prefsdocs 2> prefsdocsjunk.tmp
-	mv -f prefsdocsjunk.tmp prefsdocs.tmp
-
 # For developers
-runtest:
-	$(MAKE) NATIVE=false DEBUG=true text
-	./unison test
 
+.PHONY: repeattest
 repeattest:
 	$(MAKE) all NATIVE=false DEBUG=true UISTYLE=text
 	./unison noprofile a.tmp b.tmp -repeat foo.tmp -debug ui
 
+.PHONY: selftest
 selftest:
 	$(MAKE) all NATIVE=false DEBUG=true UISTYLE=text
 	./unison -selftest -ui text -batch
 
+.PHONY: selftestdebug
 selftestdebug:
 	$(MAKE) all NATIVE=false DEBUG=true UISTYLE=text
 	./unison -selftest -ui text -batch -debug all
 
+.PHONY: selftestremote
 selftestremote:
 	$(MAKE) all NATIVE=false DEBUG=true UISTYLE=text
 	./unison -selftest -ui text -batch test.tmp ssh://eniac.seas.upenn.edu/test.tmp
 
+.PHONY: testmerge
 testmerge:
 	$(MAKE) all NATIVE=false UISTYLE=text
 	-rm -rf a.tmp b.tmp
@@ -324,13 +171,14 @@ testmerge:
 ######################################################################
 # Tags
 
+ETAGS=etags
+
 # In Windows, tags and TAGS are the same, so make tags stops working
 # after the first invocation.  The .PHONY declaration makes it work
 # again.
 .PHONY: tags
-
 tags:
-	-if [ -f "`which $(ETAGS)`" ]; then \
+	@-if command -v $(ETAGS) > /dev/null ; then \
 	    $(ETAGS) *.mli */*.mli *.ml */*.ml */*.m *.c */*.c *.txt \
 		     *Makefile* \
 	  ; fi
@@ -343,15 +191,12 @@ TAGS:
 ######################################################################
 # Misc
 
+.PHONY: clean
 clean::
 	-$(RM) *.log *.aux *.log *.dvi *.out *.bak
 	-$(RM) -r obsolete
 	-$(RM) $(NAME) $(NAME).exe
 	-$(RM) $(NAME)-blob.o
-
-clean::
-	$(MAKE) -C ubase clean
-	$(MAKE) -C lwt clean
 	$(MAKE) -C fsmonitor/windows clean
 
 ifneq ($(strip $(UIMACDIR)),)
@@ -360,14 +205,6 @@ clean::
 	-(cd $(UIMACDIR); $(RM) -r build ExternalSettings.xcconfig)
 endif
 
-checkin:
-	$(MAKE) -C .. checkin
-
-installremote:
-	$(MAKE) UISTYLE=text
-	-unison eniac -path current/unison/trunk/src -batch
-	ssh eniac.seas.upenn.edu make -C current/unison/trunk/src installtext
-
 ####################################################################
 # Documentation strings
 
@@ -377,3 +214,4 @@ installremote:
 strings.ml:
 	echo "(* Dummy strings.ml *)" > strings.ml
 	echo "let docs = []" >> strings.ml
+
diff --git a/src/Makefile.OCaml b/src/Makefile.OCaml
index 61a8b5e..1472b2d 100644
--- a/src/Makefile.OCaml
+++ b/src/Makefile.OCaml
@@ -1,3 +1,6 @@
+# Unison file synchronizer: src/Makefile.OCaml
+# See ../LICENSE for terms.
+
 ####################################################################
 #         Makefile rules for compiling ocaml programs              #
 ####################################################################
@@ -7,48 +10,22 @@
 
 ifeq (${OSCOMP},cygwingnuc) # Define this if compiling with Cygwin GNU C
   OSARCH=win32gnuc
-  ETAGS=/bin/etags
-  buildexecutable:: win32rc/unison.res.lib
-else
-# Win32 system
-ifeq (${OSTYPE},cygwin32) # Cygwin Beta 19
-  OSARCH=win32
-  ETAGS=/bin/etags
-  buildexecutable:: win32rc/unison.res.lib
-else
-ifeq (${OSTYPE},cygwin)	  # Cygwin Beta 20
-  OSARCH=win32
-  ETAGS=/bin/etags
-  buildexecutable:: win32rc/unison.res.lib
-else
-
-# Unix system
-ifeq ($(shell uname),SunOS)
-  OSARCH=solaris
-else
-ifeq ($(shell uname),Darwin)
-  OSARCH=osx
-else
-ifeq ($(shell uname),OpenBSD)
-  OSARCH=OpenBSD
-else
-ifeq ($(shell uname),NetBSD)
-  OSARCH=NetBSD
-endif
-ifeq ($(shell uname),Linux)
-  OSARCH=Linux
-endif
-endif
-endif
-endif
-ETAGS=etags
-endif
-endif
-endif
-
-ifeq (${OSCOMP},cross) # Cross-compilation under Linux
+else ifeq (${OSCOMP},cross) # Cross-compilation under Linux
   OSARCH=win32gnuc
   EXEC_PREFIX=i686-w64-mingw32-
+else ifeq (${OSTYPE},cygwin)
+  OSARCH=win32
+else
+  # := is not POSIX but GNU make < 4.0 does not support ::= which is POSIX,
+  # and macOS ships with really outdated GNU make (4.0 released 2013).
+  # Not a problem because the entire makefile is not POSIX compatible.
+  OSARCH:=$(shell uname)
+
+  ifeq ($(OSARCH),SunOS)
+    OSARCH=solaris
+  else ifeq ($(OSARCH),Darwin)
+    OSARCH=osx
+  endif
 endif
 
 # The OCaml lib dir is used by all versions
@@ -60,7 +37,9 @@ endif
 # Better(?) version, June 2005:
 # OCAMLLIBDIR=$(shell ocamlc -v | tail -1 | sed -e 's/.* //g' | sed -e 's/\\/\//g' | tr -d '\r')
 # Another try, Feb 2011, suggested by Ron Isaacson
-OCAMLLIBDIR=$(shell ocamlc -v | tail -1 | sed -e 's/.* //g' | tr '\\' '/' | tr -d '\r')
+# OCAMLLIBDIR=$(shell ocamlc -v | tail -1 | sed -e 's/.* //g' | tr '\\' '/' | tr -d '\r')
+# 2023: "-where" has existed since OCaml 3.01 (2000)
+OCAMLLIBDIR=$(shell ocamlc -where)
 
 ## BCP (6/05) an alternative, but not quite working, version
 ## suggested by Nick Montfort:
@@ -69,32 +48,44 @@ OCAMLLIBDIR=$(shell ocamlc -v | tail -1 | sed -e 's/.* //g' | tr '\\' '/' | tr -
 # User interface style:
 #   Legal values are
 #     UISTYLE=text
-#     UISTYLE=gtk
-#     UISTYLE=gtk2
+#     UISTYLE=gtk3
 #     UISTYLE=mac
 #
 # This should be set to an appropriate value automatically, depending
 # on whether the lablgtk library is available
-LABLGTKLIB=$(OCAMLLIBDIR)/lablgtk
-LABLGTK2LIB=$(OCAMLLIBDIR)/lablgtk2
-##BCP [3/2007]: Removed temporarily, since the OSX UI is not working well
-## at the moment and we don't want to confuse people by building it by default
+#
+# For Windows, an additional UI style modifier is available, `UI_WINOS`
+#   Legal values are
+#     UI_WINOS=         # *default*; builds unison purely as a Windows console ('text') or GUI ('gtk3') application
+#     UI_WINOS=hybrid   # (with UISTYLE=gtk3) builds unison as a hybrid application (GUI application attached to a text console)
+# * ref: <https://github.com/bcpierce00/unison/issues/778>
+#
+LABLGTK3LIB=$(OCAMLLIBDIR)/lablgtk3
 ifeq ($(OSARCH),osx)
   UISTYLE=mac
 else
-  ifeq ($(wildcard $(LABLGTK2LIB)),$(LABLGTK2LIB))
-    UISTYLE=gtk2
+  ifeq ($(wildcard $(LABLGTK3LIB)),$(LABLGTK3LIB))
+    UISTYLE=gtk3
   else
-    LABLGTK2LIB=$(abspath $(OCAMLLIBDIR)/../lablgtk2)
-    ifeq ($(wildcard $(LABLGTK2LIB)),$(LABLGTK2LIB))
-      UISTYLE=gtk2
+    LABLGTK3LIB=$(abspath $(OCAMLLIBDIR)/../lablgtk3)
+    ifeq ($(wildcard $(LABLGTK3LIB)),$(LABLGTK3LIB))
+      UISTYLE=gtk3
     else
       UISTYLE=text
     endif
   endif
 endif
+ifeq ($(UISTYLE), gtk2)
+buildexecutable::
+	@echo
+	@echo "gtk2 GUI is no longer available.    Use  UISTYLE=gtk3  or don't specify any UISTYLE."
+	@echo
+	@exit 1
+endif
+
 buildexecutable::
 	@echo UISTYLE = $(UISTYLE)
+	@echo UI_WINOS = $(UI_WINOS)
 
 ####################################################################
 ### Default parameters
@@ -102,6 +93,19 @@ buildexecutable::
 # Generate backtrace information for exceptions
 CAMLFLAGS+=-g
 
+ifneq ($(strip $(CFLAGS)),)
+  CAMLCFLAGS+=-ccopt '$(CFLAGS)'
+endif
+ifneq ($(strip $(CPPFLAGS)),)
+  CAMLCFLAGS+=-ccopt '$(CPPFLAGS)'
+endif
+ifneq ($(strip $(LDFLAGS)),)
+  CAMLLDFLAGS+=-cclib '$(LDFLAGS)'
+endif
+ifneq ($(strip $(LDLIBS)),)
+  CLIBS+=-cclib '$(LDLIBS)'
+endif
+
 INCLFLAGS=-I lwt -I ubase -I system
 DEP_INCLFLAGS=-I lwt -I ubase -I system
 CAMLFLAGS+=$(INCLFLAGS)
@@ -110,14 +114,26 @@ CAMLFLAGS+=-I system/$(SYSTEM) -I lwt/$(SYSTEM)
 ifeq ($(OSARCH),win32)
   # Win32 system
   EXEC_EXT=.exe
-  OBJ_EXT=.o
-  OUTPUT_SEL=-o
+  ifeq ($(shell ocamlc -config 2> /dev/null | grep ext_obj),ext_obj: .obj)
+    OBJ_EXT=.obj
+  else
+    OBJ_EXT=.o
+  endif
+  ifeq ($(shell ocamlc -config 2> /dev/null | grep ccomp_type),ccomp_type: msvc)
+    OUTPUT_SEL=-Fo
+    CLIBS+=-cclib shell32.lib -cclib user32.lib -cclib "-link win32rc/unison.res"
+    STATICLIBS+=-cclib "-link win32rc/unison.res"
+    buildexecutable:: win32rc/unison.res
+  else
+    OUTPUT_SEL=-o
+    CLIBS+=-cclib "-link win32rc/unison.res.lib"
+    STATICLIBS+=-cclib "-link win32rc/unison.res.lib"
+    buildexecutable:: win32rc/unison.res.lib
+  endif
   CWD=.
   COBJS+=system/system_win_stubs$(OBJ_EXT) lwt/lwt_unix_stubs$(OBJ_EXT)
   WINOBJS=system/system_win.cmo
   SYSTEM=win
-  CLIBS+=-cclib "-link win32rc/unison.res.lib"
-  STATICLIBS+=-cclib "-link win32rc/unison.res.lib"
   buildexecutable::
 	@echo Building for Windows
 else
@@ -132,7 +148,7 @@ else
     SYSTEM=win
     CLIBS+=-cclib win32rc/unison.res.lib
     STATIC=false                      # Cygwin is not MinGW :-(
-    buildexecutable::
+    buildexecutable:: win32rc/unison.res.lib
 	@echo Building for Windows with Cygwin GNU C
   else
     CWD=$(shell pwd)
@@ -145,20 +161,15 @@ else
         CLIBS+=-cclib -lutil
       endif
     endif
+    ifeq ($(OSARCH),solaris)
+      # ACL functions
+      CLIBS+=-cclib -lsec
+    endif
     buildexecutable::
 	@echo Building for Unix
   endif
 endif
 
-ifeq ($(NATIVE), true)
-  COMPATCAMLFLAGS=$(subst .cmo,.cmx, $(shell ocaml $(CWD)/configure.ml))
-  COMPATOCAMLOBJS=$(subst .cmo,.cmx, $(subst .cma,.cmxa, $(shell ocaml $(CWD)/configure.ml objs)))
-else
-  COMPATCAMLFLAGS=$(shell ocaml $(CWD)/configure.ml)
-  COMPATOCAMLOBJS=$(shell ocaml $(CWD)/configure.ml objs)
-endif
-OCAMLOBJS+=$(COMPATOCAMLOBJS)
-
 .PHONY: buildexecutable
 buildexecutable::
 	@echo NATIVE = $(NATIVE)
@@ -184,31 +195,35 @@ else
   buildexecutable:: $(NAME)$(EXEC_EXT)
 endif
 
-MINOSXVERSION=10.6
 ifeq ($(OSARCH),osx)
-  CAMLFLAGS+=-ccopt -mmacosx-version-min=$(MINOSXVERSION)
-  XCODEFLAGS=-arch $(shell uname -m)
+ifeq ($(strip $(XCODEFLAGS)),)
+  XCODEFLAGS=-arch $(shell uname -m)  ## Prevent Xcode from trying to build universal binaries by default
+endif
 endif
 
 .PHONY: macexecutable
 macexecutable:
 	(cd $(UIMACDIR); $(RM) -f ExternalSettings.xcconfig ; echo MARKETING_VERSION = $(VERSION) > ExternalSettings.xcconfig ; echo OCAMLLIBDIR = $(OCAMLLIBDIR) >> ExternalSettings.xcconfig)
-ifeq ($(word 1,$(sort 4.08 $(shell ocamlc -version))),4.08)
-	(cd $(UIMACDIR); echo LIB_BIGARRAY = >> ExternalSettings.xcconfig)
-else
-	(cd $(UIMACDIR); echo LIB_BIGARRAY = -lbigarray >> ExternalSettings.xcconfig)
-endif
 	(cd $(UIMACDIR); xcodebuild $(XCODEFLAGS) SYMROOT=build)
-	$(CC) $(CFLAGS) -mmacosx-version-min=$(MINOSXVERSION) $(UIMACDIR)/cltool.c -o $(UIMACDIR)/build/Default/Unison.app/Contents/MacOS/cltool -framework Carbon
+	$(CC) $(CFLAGS) $(UIMACDIR)/cltool.c -o $(UIMACDIR)/build/Default/Unison.app/Contents/MacOS/cltool -framework Carbon
+	codesign --remove-signature $(UIMACDIR)/build/Default/Unison.app
+	codesign --force --sign - $(UIMACDIR)/build/Default/Unison.app/Contents/MacOS/cltool
+	codesign --force --sign - --entitlements $(UIMACDIR)/build/uimac*.build/Default/uimac.build/Unison.app.xcent $(UIMACDIR)/build/Default/Unison.app
+	codesign --verify --deep --strict $(UIMACDIR)/build/Default/Unison.app
+# cltool was added into the .app after it was signed, so the signature is now
+# broken. It must be removed, cltool separately signed, and then the entire
+# .app (re-)signed.
+
 
 # OCaml objects for the bytecode version
 # File extensions will be substituted for the native code version
 
 OCAMLOBJS += \
+          ubase/umarshal.cmo \
           ubase/rx.cmo \
 	  \
           unicode_tables.cmo unicode.cmo bytearray.cmo \
-          $(WINOBJS) system/system_generic.cmo \
+          system/system_generic.cmo $(WINOBJS) \
           system/$(SYSTEM)/system_impl.cmo \
           system.cmo \
           \
@@ -219,12 +234,12 @@ OCAMLOBJS += \
           lwt/pqueue.cmo lwt/lwt.cmo lwt/lwt_util.cmo \
           lwt/$(SYSTEM)/lwt_unix_impl.cmo lwt/lwt_unix.cmo \
           \
-          uutil.cmo case.cmo pred.cmo \
+          features.cmo uutil.cmo case.cmo pred.cmo terminal.cmo \
           fileutil.cmo name.cmo path.cmo fspath.cmo fs.cmo fingerprint.cmo \
-          abort.cmo osx.cmo external.cmo fswatch.cmo \
+          abort.cmo osx.cmo fswatch.cmo propsdata.cmo \
           props.cmo fileinfo.cmo os.cmo lock.cmo clroot.cmo common.cmo \
-          tree.cmo checksum.cmo terminal.cmo \
-          transfer.cmo xferhint.cmo remote.cmo globals.cmo fswatchold.cmo \
+          tree.cmo checksum.cmo transfer.cmo xferhint.cmo \
+          remote.cmo external.cmo negotiate.cmo globals.cmo fswatchold.cmo \
           fpcache.cmo update.cmo copy.cmo stasher.cmo \
 	  files.cmo sortri.cmo recon.cmo transport.cmo \
           strings.cmo uicommon.cmo uitext.cmo test.cmo
@@ -233,9 +248,10 @@ OCAMLOBJS+=main.cmo
 
 # OCaml libraries for the bytecode version
 # File extensions will be substituted for the native code version
-OCAMLLIBS+=unix.cma str.cma bigarray.cma
+OCAMLLIBS+=unix.cma str.cma
+INCLFLAGS+=-I +unix -I +str
 
-COBJS+=osxsupport$(OBJ_EXT) pty$(OBJ_EXT) bytearray_stubs$(OBJ_EXT) hash_compat$(OBJ_EXT)
+COBJS+=osxsupport$(OBJ_EXT) pty$(OBJ_EXT) bytearray_stubs$(OBJ_EXT) hash_compat$(OBJ_EXT) props_xattr$(OBJ_EXT) props_acl$(OBJ_EXT)
 
 ########################################################################
 ### User Interface setup
@@ -245,11 +261,11 @@ ifeq ($(UISTYLE), text)
   OCAMLOBJS+=linktext.cmo
 endif
 
-## New Mac UI, 2014 version
+## Mac UI
 ifeq ($(UISTYLE),mac)
-  OCAMLOBJS+=uimacbridgenew.cmo
+  OCAMLOBJS+=uimacbridge.cmo
   OCAMLLIBS+=threads.cma
-  INCLFLAGS+=-thread
+  INCLFLAGS+=-I +threads
 endif
 
 ## Graphic UI
@@ -268,29 +284,25 @@ endif
 # console when click-started
 ifeq ($(OSARCH), win32)
   ifneq ($(UISTYLE), text)
-#    COBJS+=winmain.c
-    CFLAGS+=-ccopt "-link -Wl,--subsystem,windows"
+    ifneq ($(UI_WINOS), hybrid)
+      # COBJS+=winmain.c
+      CAMLLDFLAGS+=-cclib "-link -Wl,--subsystem,windows"
+    endif
   endif
 endif
 
-# Gtk GUI
-ifeq ($(UISTYLE), gtk)
-  CAMLFLAGS+=-I +lablgtk
-  OCAMLOBJS+=pixmaps.cmo uigtk.cmo linkgtk.cmo
-  OCAMLLIBS+=lablgtk.cma
-endif
-
-# Gtk2 GUI
+# Gtk3 GUI
 OCAMLFIND := $(shell command -v ocamlfind 2> /dev/null)
 
-ifeq ($(UISTYLE), gtk2)
+ifeq ($(UISTYLE), gtk3)
   ifndef OCAMLFIND
-    CAMLFLAGS+=-I +lablgtk2
+    CAMLFLAGS+=-I +lablgtk3 -I +cairo2
   else
-    CAMLFLAGS+=$(shell $(OCAMLFIND) query -i-format lablgtk2 )
+    CAMLFLAGS+=$(shell $(OCAMLFIND) query -i-format lablgtk3 )
+    CAMLFLAGS+=$(shell $(OCAMLFIND) query -i-format cairo2 )
   endif
-  OCAMLOBJS+=pixmaps.cmo uigtk2.cmo linkgtk2.cmo
-  OCAMLLIBS+=lablgtk.cma
+  OCAMLOBJS+=pixmaps.cmo uigtk3.cmo linkgtk3.cmo
+  OCAMLLIBS+=lablgtk3.cma cairo.cma
 endif
 
 ########################################################################
@@ -322,64 +334,41 @@ DEP_INCLFLAGS+=-I fsmonitor -I fsmonitor/linux -I fsmonitor/solaris -I fsmonitor
 ### Static build setup
 
 ifeq ($(STATIC), true)
-  CFLAGS+=-cclib -static
+  CAMLLDFLAGS+=-cclib -static
 endif
 
 ####################################################################
 ### Dependencies
 
 # Include an automatically generated list of dependencies
-include .depend
-# Additional dependencied depending on the system
+-include .depend
+# Additional dependencies depending on the system
 system.cmo fspath.cmo fs.cmo: system/$(SYSTEM)/system_impl.cmo
 system.cmx fspath.cmx fs.cmx: system/$(SYSTEM)/system_impl.cmx
 lwt/lwt_unix.cmo: lwt/$(SYSTEM)/lwt_unix_impl.cmo
 lwt/lwt_unix.cmx: lwt/$(SYSTEM)/lwt_unix_impl.cmx
 
-ifeq ($(OSARCH), OpenBSD)
-  ifeq ($(shell echo type ocamldot | ksh), file)
-    OCAMLDOT=true
-  endif
-else
-  ifeq ($(shell echo type -t ocamldot | bash), file)
-    OCAMLDOT=true
-  endif
-endif
-
-ifeq ($(OSARCH), NetBSD)
-  OCAMLDOT=false
-endif
-
-# Rebuild dependencies (must be invoked manually)
 .PHONY: depend
 depend::
-	ocamldep $(DEP_INCLFLAGS) *.mli *.ml */*.ml */*.mli */*/*.ml */*/*.mli > .depend
-ifdef OCAMLDOT
-	echo 'digraph G {' > dot.tmp
+	ocamlc -depend $(DEP_INCLFLAGS) *.mli *.ml */*.ml */*.mli */*/*.ml */*/*.mli > .depend
+
+.PHONY: dependgraph
+dependgraph: depend
+	echo 'digraph G {' > .depend.dot.tmp
 	echo '{ rank = same; "Fileinfo"; "Props"; "Fspath"; "Os"; "Path"; }'\
-           >>dot.tmp
-	echo '{ rank = same; "Uitext"; "Uigtk"; }'>>dot.tmp
+           >>.depend.dot.tmp
+	echo '{ rank = same; "Uitext"; "Uigtk"; }'>>.depend.dot.tmp
 	echo '{ rank = same; "Recon"; "Update"; "Transport"; "Files"; }'\
-           >>dot.tmp
-	echo '{ rank = same; "Tree"; "Safelist"; }'>>dot.tmp
-	echo '{ rank = same; "Uarg"; "Prefs"; }'>>dot.tmp
-	ocamldot .depend | tail -n +2 >> dot.tmp
-	-dot -Tps -o DEPENDENCIES.ps dot.tmp
-endif
+           >>.depend.dot.tmp
+	echo '{ rank = same; "Tree"; "Safelist"; }'>>.depend.dot.tmp
+	echo '{ rank = same; "Uarg"; "Prefs"; }'>>.depend.dot.tmp
+	echo 'Attempting to use ocamldot and dot to produce a dependency graph:'
+	-ocamldot .depend | tail -n +2 >> .depend.dot.tmp
+	-dot -Tps -o DEPENDENCIES.ps .depend.dot.tmp
 
 ####################################################################
 ### Compilation boilerplate
 
-ifeq ($(DEBUGGING), false)
-  ifneq ($(OSARCH), win32)
-    ifneq ($(OSARCH), osx)
-      # Strip the binary (does not work with MS compiler; might not work
-      # under OSX)
-      CFLAGS+=-cclib -Wl,-s
-    endif
-  endif
-endif
-
 ifeq ($(PROFILING), true)
   OCAMLC=$(EXEC_PREFIX)ocamlcp
 else
@@ -395,7 +384,6 @@ ifeq ($(NATIVE), true)
     CAMLFLAGS+=-p
     CLIBS+=-cclib -ldl
   endif
-  CAMLLDFLAGS=
 
   CAMLOBJS=$(subst .cmo,.cmx, $(subst .cma,.cmxa, $(OCAMLOBJS)))
   CAMLLIBS=$(subst .cma,.cmxa, $(OCAMLLIBS))
@@ -404,10 +392,13 @@ else
   ## Set up for bytecode compilation
 
   CAMLC=$(OCAMLC)
-  ifeq ($(DEBUGGING), true)
-    CAMLFLAGS+=-g
+  _COMPLETE_EXE=$(shell ocamlc -output-complete-exe 2>&1)
+  # -output-complete-exe is available since OCaml 4.10
+  ifneq ($(strip $(_COMPLETE_EXE)),)
+    CAMLLDFLAGS+=-custom
+  else
+    CAMLLDFLAGS+=-output-complete-exe  # can safely strip the binary
   endif
-  CAMLLDFLAGS=-custom
 
   CAMLOBJS=$(OCAMLOBJS)
   CAMLLIBS=$(OCAMLLIBS)
@@ -418,9 +409,10 @@ WINDRES := $(or ${EXEC_PREFIX},$(filter i686-w64-mingw32- x86_64-w64-mingw32-,$(
 ##$(info windres='${WINDRES}')
 
 win32rc/unison.res: win32rc/unison.rc win32rc/U.ico
-	$(WINDRES) win32rc/unison.rc win32rc/unison.res
+	rc win32rc/unison.rc
 
-win32rc/unison.res.lib: win32rc/unison.res
+win32rc/unison.res.lib: win32rc/unison.rc win32rc/U.ico
+	$(WINDRES) win32rc/unison.rc win32rc/unison.res
 	$(WINDRES) win32rc/unison.res win32rc/unison.res.lib
 
 %.ml: %.mll
@@ -429,53 +421,32 @@ win32rc/unison.res.lib: win32rc/unison.res
 
 %.cmi : %.mli
 	@echo "$(CAMLC): $< ---> $@"
-	$(CAMLC) $(CAMLFLAGS) $(COMPATCAMLFLAGS) -c $(CWD)/$<
-
-fswatch.cmi : ubase/prefs.cmi
+	$(CAMLC) $(CAMLFLAGS) -c $(CWD)/$<
 
-compat%.cmo: compat%.ml
+%.cmo: %.ml
 	@echo "$(OCAMLC): $< ---> $@"
-	$(OCAMLC) $(COMPATCAMLFLAGS) -c $(CWD)/$<
+	$(OCAMLC) $(CAMLFLAGS) -c $(CWD)/$<
 
-%.cmo: %.ml $(COMPATOCAMLOBJS)
-	@echo "$(OCAMLC): $< ---> $@"
-	$(OCAMLC) $(CAMLFLAGS) $(COMPATCAMLFLAGS) -c $(CWD)/$<
-
-compat%.cmx: compat%.ml
+%.cmx: %.ml
 	@echo "$(OCAMLOPT): $< ---> $@"
-	$(OCAMLOPT) $(COMPATCAMLFLAGS) -c $(CWD)/$<
-
-%.cmx: %.ml $(COMPATOCAMLOBJS)
-	@echo "$(OCAMLOPT): $< ---> $@"
-	$(OCAMLOPT) $(CAMLFLAGS) $(COMPATCAMLFLAGS) -c $(CWD)/$<
+	$(OCAMLOPT) $(CAMLFLAGS) -c $(CWD)/$<
 
 %.o %.obj: %.c
 	@echo "$(CAMLC): $< ---> $@"
-	$(CAMLC) $(CAMLFLAGS) -ccopt $(OUTPUT_SEL)$(CWD)/$@ -c $(CWD)/$<
+	$(CAMLC) $(CAMLFLAGS) $(CAMLCFLAGS) -ccopt $(OUTPUT_SEL)$(CWD)/$@ -c $(CWD)/$<
 
 $(NAME)$(EXEC_EXT): $(CAMLOBJS) $(COBJS)
 	@echo Linking $@
-	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CFLAGS) $(CAMLLIBS) $^ $(CLIBS)
+	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CAMLCFLAGS) $(CAMLLIBS) $^ $(CLIBS)
 
 # Unfortunately -output-obj does not put .o files into the output, only .cmx
 # files, so we have to use $(LD) to take care of COBJS.
 $(NAME)-blob.o: $(CAMLOBJS) $(COBJS)
 	@echo Linking $@
-	$(CAMLC) -dstartup -output-obj -verbose -cclib -keep_private_externs $(CAMLFLAGS) -o u-b.o $(CFLAGS) $(CAMLLIBS) $(CLIBS) $(CAMLOBJS)
+	$(CAMLC) -dstartup -output-obj -verbose -cclib -keep_private_externs $(CAMLFLAGS) -o u-b.o $(CAMLCFLAGS) $(CAMLLDFLAGS) $(CAMLLIBS) $(CLIBS) $(CAMLOBJS)
 	$(LD) -r -keep_private_externs -o $@ u-b.o $(COBJS)
 	$(RM) u-b.o
 
-
-# Original:
-# $(NAME)-blob.o: $(CAMLOBJS) $(COBJS)
-# 	@echo Linking $@
-# 	$(CAMLC) -output-obj -verbose $(CAMLFLAGS) -o u-b.o $(CFLAGS) $(CAMLLIBS) $(CLIBS) $(CAMLOBJS)
-# 	$(LD) -r -o $@ u-b.o $(COBJS)
-# 	$(RM) u-b.o
-
-%$(EXEC_EXT): %.ml
-	$(OCAMLC) -verbose -o $@ $^
-
 ######################################################################
 ### Misc
 
@@ -484,10 +455,15 @@ clean::
 	-$(RM) -r *.o core gmon.out *~ .*~
 	-$(RM) -r *.obj *.lib *.exp
 	-$(RM) -r *.tmp *.bak?.tmp .*.bak?.tmp
-	-$(RM) system/*.cm[iox] system/*.{o,obj} system/win/*~
-	-$(RM) system/generic/*.cm[iox] system/generic/*.{o,obj} system/generic/*~
-	-$(RM) system/win/*.cm[iox] system/win/*.{o,obj} system/win/*~
-	-$(RM) fsmonitor/*.cm[iox] fsmonitor/*.{o,obj}
+	-$(RM) system/*.cm[iox] system/*.o system/*.obj system/win/*~
+	-$(RM) system/generic/*.cm[iox] system/generic/*.o system/generic/*.obj system/generic/*~
+	-$(RM) system/win/*.cm[iox] system/win/*.o system/win/*.obj system/win/*~
+	-$(RM) fsmonitor/*.cm[iox] fsmonitor/*.o fsmonitor/*.obj
+	-$(RM) .depend.dot.tmp DEPENDENCIES.ps
+	-$(RM) ubase/*.cm[ioxa] ubase/*.cmxa ubase/*.a ubase/*.o ubase/*~ ubase/*.bak
+	-$(RM) lwt/*.cm[ioxa] lwt/*.cmxa lwt/*.a lwt/*.o lwt/*.obj lwt/*~ lwt/*.bak
+	-$(RM) lwt/generic/*.cm[ioxa] lwt/generic/*.cmxa lwt/generic/*.a lwt/generic/*.o lwt/generic/*.obj lwt/generic/*~ lwt/generic/*.bak
+	-$(RM) lwt/win/*.cm[ioxa] lwt/win/*.cmxa lwt/win/*.a lwt/win/*.o lwt/win/*.obj lwt/win/*~ lwt/win/*.bak
 
 .PHONY: paths
 paths:
diff --git a/src/Makefile.ProjectInfo b/src/Makefile.ProjectInfo
index 7924ab0..5542191 100644
--- a/src/Makefile.ProjectInfo
+++ b/src/Makefile.ProjectInfo
@@ -1,5 +1,5 @@
-MAJORVERSION=2.51
+MAJORVERSION=2.53
 
-VERSION=2.51.5
+VERSION=2.53.3
 
 NAME=unison
diff --git a/src/ROADMAP.txt b/src/ROADMAP.txt
index 4ca198e..6c13bdb 100644
--- a/src/ROADMAP.txt
+++ b/src/ROADMAP.txt
@@ -30,7 +30,7 @@ look next.  Here's a summary of the most interesting modules:
 
 The files linktext.ml and linkgtk.ml contain linking commands for
 assembling unision with either a textual or a graphical user interface.
-(The Main module, which takes the UI as a paramter, is the only part of
+(The Main module, which takes the UI as a parameter, is the only part of
 the program that is functorized.)
 
 The module Remote handles RPC communication between clients and remote
diff --git a/src/TODO.txt b/src/TODO.txt
index b91dbbc..807e604 100644
--- a/src/TODO.txt
+++ b/src/TODO.txt
@@ -25,7 +25,7 @@ promise that anybody is going to implement it!)
          - sort the list
          - if there are any adjacent pairs where the first is a prefix of the
            second, drop the second and mark the first as deep
-         - go through the list and drop any item for whioch any PREFIX of
+         - go through the list and drop any item for which any PREFIX of
            its path matches 'ignore' and doesn't match 'ignorenot'
      - bulletproof, handling fatal errors and restarting completely from
        scratch if necessary
@@ -83,12 +83,6 @@ It would be nice if Unison could have the "power" to copy write-protected
 
 *** See if we can get rid of some Osx.XXX stuff (e.g. ressLength!?)
 
-*** Overlapping paths
-    If one -path argument is a prefix of another, the same files will get
-    scanned twice, found to need transferring twice, and transferred twice, but
-    the first transfer messes up the second.  The fix would be to throw
-    away -path arguments that are suffixes of other ones.
-
 * There is no way of selecting files with wildchar. I had to use
      ignorenot = Name opt/root/.unison/*.prf
      ignore = Name opt/root/.unison/*
@@ -100,7 +94,7 @@ It would be nice if Unison could have the "power" to copy write-protected
 * If a directory does not exist in one of the host, unison (for
   security reasons, which I like) pops up a window and Quit is the only
   option. I would expect a message stating mere clearly on which host and
-  direcory and an option to create that directory. I had recently to make
+  directory and an option to create that directory. I had recently to make
   a lot of reinstalls and new pendrives and it took a long time to create
   all those dirs. Someone in the list even made a script to do the job!!!
 
@@ -196,10 +190,6 @@ should strip symbols from binary files in 'make exportnative'
 * DOCUMENTATION
 * =============
 
-** Put a little more order on the flags and preferences -- e.g.,
-    organize them into "basic preferences", "advanced preferences,"
-    "expert preferences," etc.  Requires hacking the Uarg module.
-
 ** Add something to docs about how to use 'rootalias'.  Include an
    explanation of the semantics, a couple of examples, and a suggestion
    for how to debug what it's doing by turning on appropriate debugging
@@ -265,13 +255,6 @@ should strip symbols from binary files in 'make exportnative'
        - otherwise, put them in a central place if one is given
        - Update.incrVersionsOfBackups should not be externally visible
 
-*** Consider altering the socket method, so the server accepts connections
-    only on a particular address? This would be very useful, because many people
-    tunnel unison over an OpenVPN Link, and this software works with virtual
-    devices and additional IP addresses on it. If unison would accept
-    connections only on the virtual device, the security would be enhanced,
-    because the OpenVPN key should be unavailable for the black hats.
-
 *** unison -help doesn't go to stdout so it's hard to pipe it into less
     ===> Probably *all* output should go to stdout, not stderr (but maybe
          we need a switch to recover the current behavior)
@@ -282,7 +265,7 @@ should strip symbols from binary files in 'make exportnative'
 *** If a root resides on a `host' with an ever and unpredictably changing
     host name (like a public login cluster with dozens of machines and a
     shared file system), listing each possible host name for this root is
-    not feasible.  The ability of specifing patterns in rootaliases would
+    not feasible.  The ability of specifying patterns in rootaliases would
     help a lot in this case.  I'm thinking of something like this:
     rootalias = //.*//afs/cern.ch/user/n/nagya ->
     //cern.ch//afs/cern.ch/user/n/nagya [NAGY Andras <nagya@inf.elte.hu>,
@@ -302,14 +285,6 @@ should strip symbols from binary files in 'make exportnative'
     offer to delete them *for* the user, rather than forcing the user to
     delete them manually.
 
-*** A switch to include NTFS ACE/ACL file permissions to be copied when
-    copying from one NTFS location to another NTFS location. As I
-    mentioned this is less generic, but of fundamental usefullness in
-    Windows usage, as NTFS permissions are absolutely essential in many
-    backup/replication situations in Windows systems. Robocopy has the
-    /SEC switch, but Unison is a far better tool, and I was hoping in that
-    light that Unison could implement the rights/permissions stuff also.
-
 *** There is no command-line argument to tell Unison where the .unison
     directory is; Unison finds it in the environment or not at all.  I was
     able to workaround this with a symbolic link to put .unison where it was
@@ -401,7 +376,7 @@ should strip symbols from binary files in 'make exportnative'
   specify a user (and similarly a group) to unison.  It would be
   interpreted in a special way: if a file is owned by this user, unison
   will rather consider that the owner of the file is undefined.  So, when
-  a file owned by an unkown user is synchronized, the file owner is set
+  a file owned by an unknown user is synchronized, the file owner is set
   to the default user.  Then, on the next synchronizations, unison will
   consider that the owner has not been propagated and try again.  [Should
   be easy once the reconciler is made more modular]
@@ -418,13 +393,6 @@ should strip symbols from binary files in 'make exportnative'
   start up the GUI from the command line (I would argue that people using the
   GUI regularly will start it anyway by double clicking the app)
 
-Would be nice to have the Unison log file relative to my home directory,
- like this
-       logfile = ~/.unision/log
-  or
-       logfile = $HOME/.unision/log
-  (We should do this for *all* files that the user specifies.)
-
 add a switch '-logerrors' that makes unison log error messages to a
   separate file in addition to the standard logfile
 
@@ -533,9 +501,6 @@ If the connection to the server goes away and then comes back up, it
   would be nice if Unison would transparently re-establish it (at least,
   when this makes sense!)
 
-maybe put backup files somewhere other than in the replica (e.g. in
-  $HOME/tmp, or controlled by preference)
-
 Better documentation of the -backups flag, and a way to expire old backups
 
 Add a preference that makes the reconciler ignore prefs-only differences
@@ -577,13 +542,6 @@ the local host and which side the remote host is.
 * USER INTERFACE
 * ==============
 
-** In menu Actions
-    - show Diff applies to the current line, while
-    - revert to unision's recommandation applies to all lines
-   Should be clearer and/or homogeneous behavior.
-   I would also like to have "revert to unision's recommandation" for the
-   current line.
-
 ** in gtk ui, display green checkmark next to finished items even if their
    direction indicates a conflict; do not list such items as "skipped" at
    the end
@@ -663,12 +621,6 @@ the local host and which side the remote host is.
   This might be a good use for "tool tips," if I knew how to make them work
   using lablGTK.
 
-* After clicking "Create new profile" in the initial profile window and
-  giving a name for the new profile, it is confusing to get dumped back
-  into the profile window again and have to explicitly select the new
-  profile.  Would be better to skip this step and go straight into
-  filling in its fields.
-
 * The menu help for left/right arrow both said `transfer local to local'.
   Not helpful.  The items in question are pathnames, which you might not
   have to abbreviate.  To save space one might consider replacing any
@@ -691,10 +643,6 @@ the local host and which side the remote host is.
 * [Jamey Leifer] I think "unison -doc" should be mapped to "unison
   -doc topics" and the error message for the former eliminated.
 
-* [Jamey Leifer] Typing "unison" results in the Profiles box
-  ("Select an existing profile...").  I think the help topics should be
-  available here.
-
 Unison's gui offers an `Actions' menu with a variety of features
   regarding preferences.  I would love to see an action with the following
   semantics: if the two files differ only in their modification time,
@@ -747,9 +695,6 @@ If I have a change I look at the detail window. It would be nice to be
 Also, it would be nice to highlight in the detailed window the
    elements that have changed.
 
-Make it possible to select a bunch of conflicts at the same time and
-  override them all together
-
 The UI window should display the current roots somewhere.
 
 There should be a -geometry command-line interface, following the usual X
@@ -787,9 +732,6 @@ It would be nice if the initial 'usage' message were not so long.  Maybe
 The UI for the diff functionality needs some polishing.  (Also, it should
 be merged with the new "merge" functionality.)
 
-consider separating switches into 'ordinary' and 'expert' categories,
-  documented in separate sections
-
 would be nice to be able to "Proceed" just the selected line
 
 might be nice if the GUI would beep when finished syncing (needs to be
@@ -839,10 +781,6 @@ Rewrite recon.ml in a more modular way.  Probably, have for each property
   replicas, and returning in what the synchronization operation should be
   (nothing, left, right, conflict); a combinator then merge the results.
 
-It would be good to have a graphical interface allowing management and
-  editing of profiles, ignore patterns, etc.  Or, less ambitiously, just
-  have UI options for all command-line options (killServer)
-
 How about a facility so that you can specify more than one pair of
   file systems for a single invocation of Unison? This would be like
   calling Unison multiple times, except that it would ask all the
diff --git a/src/abort.ml b/src/abort.ml
index 8e0e1f1..2d8e8e0 100644
--- a/src/abort.ml
+++ b/src/abort.ml
@@ -21,7 +21,8 @@ let debug = Trace.debug "abort"
 
 let maxerrors =
   Prefs.createInt "maxerrors" 1
-    "!maximum number of errors before a directory transfer is aborted"
+    ~category:(`Advanced `General)
+    "maximum number of errors before a directory transfer is aborted"
     "This preference controls after how many errors Unison aborts a \
      directory transfer.  Setting it to a large number allows Unison \
      to transfer most of a directory even when some files fail to be \
@@ -59,12 +60,20 @@ let all () = abortAll := true
 
 (****)
 
+let isAll () = !abortAll
+
+let checkAll () =
+  if !abortAll then raise (Util.Transient "Aborted by user request")
+
 let check id =
   debug (fun() -> Util.msg "Checking line %s\n" (Uutil.File.toString id));
-  if !abortAll || errorCount id >= Prefs.read maxerrors then begin
+  checkAll ();
+  if errorCount id >= Prefs.read maxerrors then begin
     debug (fun() ->
       Util.msg "Abort failure for line %s\n" (Uutil.File.toString id));
     raise (Util.Transient "Aborted")
   end
 
-let testException e = (e = Util.Transient "Aborted")
+let testException e =
+  (e = Util.Transient "Aborted") ||
+  (e = Util.Transient "Aborted by user request")
diff --git a/src/abort.mli b/src/abort.mli
index eca5ed4..f63d7ef 100644
--- a/src/abort.mli
+++ b/src/abort.mli
@@ -7,9 +7,13 @@ val reset : unit -> unit
 val file : Uutil.File.t -> unit
 val all : unit -> unit
 
+(* Check whether stop of all transfers has been requested. *)
+val isAll : unit -> bool
+val checkAll : unit -> unit (* Raises a transient exception *)
+
 (* Check whether an item is being aborted.  A transient exception is
    raised if this is the case. *)
 val check : Uutil.File.t -> unit
 
-(* Test whether the exeption is an abort exception. *)
+(* Test whether the exception is an abort exception. *)
 val testException : exn -> bool
diff --git a/src/bytearray.ml b/src/bytearray.ml
index c355175..eb3cee1 100644
--- a/src/bytearray.ml
+++ b/src/bytearray.ml
@@ -19,6 +19,8 @@ open Bigarray
 
 type t = (char, int8_unsigned_elt, c_layout) Array1.t
 
+let m = Umarshal.bytearray
+
 let length = Bigarray.Array1.dim
 
 let create l = Bigarray.Array1.create Bigarray.char Bigarray.c_layout l
@@ -36,13 +38,13 @@ let unsafe_blit_to_string a i s j l =
 *)
 
 external unsafe_blit_from_string : string -> int -> t -> int -> int -> unit
-  = "ml_blit_string_to_bigarray" "noalloc"
+  = "ml_blit_string_to_bigarray" [@@noalloc]
 
 external unsafe_blit_from_bytes : bytes -> int -> t -> int -> int -> unit
-  = "ml_blit_bytes_to_bigarray" "noalloc"
+  = "ml_blit_bytes_to_bigarray" [@@noalloc]
 
 external unsafe_blit_to_bytes : t -> int -> bytes -> int -> int -> unit
-  = "ml_blit_bigarray_to_bytes" "noalloc"
+  = "ml_blit_bigarray_to_bytes" [@@noalloc]
 
 let to_string a =
   let l = length a in
diff --git a/src/bytearray.mli b/src/bytearray.mli
index 8bd5528..a76c0be 100644
--- a/src/bytearray.mli
+++ b/src/bytearray.mli
@@ -4,6 +4,8 @@
 type t =
   (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
 
+val m : t Umarshal.t
+
 val create : int -> t
 
 val length : t -> int
diff --git a/src/bytearray_stubs.c b/src/bytearray_stubs.c
index 66c64ee..d2a8ffc 100644
--- a/src/bytearray_stubs.c
+++ b/src/bytearray_stubs.c
@@ -3,60 +3,55 @@
 
 #include <string.h>
 
-#include "caml/intext.h"
-#include "caml/bigarray.h"
-#include "caml/memory.h"
+#include <caml/intext.h>
+#include <caml/bigarray.h>
+#include <caml/memory.h>
 
 CAMLprim value ml_marshal_to_bigarray(value v, value flags)
 {
+  CAMLparam2(v, flags);
   char *buf;
   intnat len;
-  output_value_to_malloc(v, flags, &buf, &len);
-  return alloc_bigarray(BIGARRAY_UINT8 | BIGARRAY_C_LAYOUT | BIGARRAY_MANAGED,
-                        1, buf, &len);
+  caml_output_value_to_malloc(v, flags, &buf, &len);
+  CAMLreturn(
+      caml_ba_alloc_dims(CAML_BA_UINT8 | CAML_BA_C_LAYOUT | CAML_BA_MANAGED,
+          1, buf, len));
 }
 
 
-#define Array_data(a, i) (((char *) a->data) + Long_val(i))
-
-#ifndef Bytes_val
-#define Bytes_val(x) ((unsigned char *) Bp_val(x))
-#endif
-
+#define Array_data(a, i) (((char *) Caml_ba_data_val(a)) + Long_val(i))
 
 CAMLprim value ml_unmarshal_from_bigarray(value b, value ofs)
 {
-  CAMLparam1(b); /* Holds [b] live until unmarshalling completes. */
-  value result;
-  struct caml_bigarray *b_arr = Bigarray_val(b);
-  result = input_value_from_block (Array_data (b_arr, ofs),
-                                 b_arr->dim[0] - Long_val(ofs));
+  CAMLparam2(b, ofs);
+  CAMLlocal1(result);
+  result = caml_input_value_from_block(Array_data(b, ofs),
+               Caml_ba_array_val(b)->dim[0] - Long_val(ofs));
   CAMLreturn(result);
 }
 
-CAMLprim value ml_blit_string_to_bigarray
+CAMLprim value ml_blit_bytes_to_bigarray
 (value s, value i, value a, value j, value l)
 {
-  const char *src = String_val(s) + Long_val(i);
-  char *dest = Array_data(Bigarray_val(a), j);
+  CAMLparam5(s, i, a, j, l);
+  unsigned char *src = Bytes_val(s) + Long_val(i);
+  char *dest = Array_data(a, j);
   memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  CAMLreturn(Val_unit);
 }
 
-CAMLprim value ml_blit_bytes_to_bigarray
+CAMLprim value ml_blit_string_to_bigarray
 (value s, value i, value a, value j, value l)
 {
-  unsigned char *src = Bytes_val(s) + Long_val(i);
-  char *dest = Array_data(Bigarray_val(a), j);
-  memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  return ml_blit_bytes_to_bigarray(s, i, a, j, l);
 }
 
 CAMLprim value ml_blit_bigarray_to_bytes
 (value a, value i, value s, value j, value l)
 {
-  char *src = Array_data(Bigarray_val(a), i);
+  CAMLparam5(a, i, s, j, l);
+  char *src = Array_data(a, i);
   unsigned char *dest = Bytes_val(s) + Long_val(j);
   memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  CAMLreturn(Val_unit);
 }
diff --git a/src/case.ml b/src/case.ml
index aa7ddef..36f70f7 100644
--- a/src/case.ml
+++ b/src/case.ml
@@ -22,7 +22,8 @@
 (* the hosts is case insensitive.                                            *)
 let caseInsensitiveMode =
   Prefs.createBoolWithDefault "ignorecase"
-    "!identify upper/lowercase filenames (true/false/default)"
+    ~category:(`Advanced `Sync)
+    "identify upper/lowercase filenames (true/false/default)"
     ("When set to {\\tt true}, this flag causes Unison to treat "
      ^ "filenames as case insensitive---i.e., files in the two "
      ^ "replicas whose names differ in (upper- and lower-case) `spelling' "
@@ -37,33 +38,37 @@ let caseInsensitiveMode =
    to the other host during initialization *)
 let someHostIsInsensitive =
   Prefs.createBool "someHostIsInsensitive" false
+    ~category:(`Internal `Pseudo)
     "*Pseudo-preference for internal use only" ""
 
 let unicode =
   Prefs.createBoolWithDefault "unicode"
-    "!assume Unicode encoding in case insensitive mode"
+    ~category:(`Advanced `General)
+    "assume Unicode encoding in case insensitive mode"
     "When set to {\\tt true}, this flag causes Unison to perform \
      case insensitive file comparisons assuming Unicode encoding.  \
      This is the default.  When the flag is set to {\\tt false}, \
-     a Latin 1 encoding is assumed.  When Unison runs in case sensitive \
-     mode, this flag only makes a difference if one host is running \
-     Windows or Mac OS X.  Under Windows, the flag selects between using \
-     the Unicode or 8bit Windows API for accessing the filesystem. \
+     Latin 1 encoding is assumed (this means that all bytes that are \
+     not letters in Latin 1 encoding will be compared byte-for-byte, \
+     even if they may be valid characters in some other encoding).  \
+     When Unison runs in case sensitive mode, this flag only makes \
+     a difference if one host is running Mac OS X.  \
      Under Mac OS X, it selects whether comparing the filenames up to \
      decomposition, or byte-for-byte."
 
 let unicodeEncoding =
   Prefs.createBool "unicodeEnc" false
+    ~category:(`Internal `Pseudo)
     "*Pseudo-preference for internal use only" ""
 
 let useUnicode () =
   let pref = Prefs.read unicode in
   pref = `True || pref = `Default
 
-let useUnicodeAPI = useUnicode
-
 let unicodeCaseSensitive =
-  Prefs.createBool "unicodeCS" ~local:true false
+  Prefs.createBool "unicodeCS" false
+    ~category:(`Internal `Pseudo)
+    ~local:true
     "*Pseudo-preference for internal use only" ""
 
 (* During startup the client determines the case sensitivity of each root.   *)
diff --git a/src/case.mli b/src/case.mli
index 8db5621..919dba6 100644
--- a/src/case.mli
+++ b/src/case.mli
@@ -3,7 +3,6 @@
 
 val caseInsensitiveMode : [`True|`False|`Default] Prefs.t
 val unicodeEncoding : bool Prefs.t
-val useUnicodeAPI : unit -> bool
 
 type mode = Sensitive | Insensitive | UnicodeSensitive | UnicodeInsensitive
 
diff --git a/src/clroot.ml b/src/clroot.ml
index 1672e7d..1008dbf 100644
--- a/src/clroot.ml
+++ b/src/clroot.ml
@@ -27,11 +27,14 @@
   protocol ::= file
             |  socket
             |  ssh
-            |  rsh
 
   user ::= [-_a-zA-Z0-9]+
 
   host ::= [-_a-zA-Z0-9.]+
+        |  \[ [a-f0-9:.]+ zone? \]     IPv6 literals (no future format).
+        |  { [^}]+ }                   For Unix domain sockets only.
+
+  zone ::= %[-_a-zA-Z0-9~%.]+
 
   port ::= [0-9]+
 
@@ -44,7 +47,7 @@ type clroot =
     ConnectLocal of
           string option (* root *)
   | ConnectByShell of
-          string        (* shell = "rsh" or "ssh" *)
+          string        (* shell = "ssh" *)
         * string        (* name of host *)
         * string option (* user name to log in as *)
         * string option (* port *)
@@ -55,7 +58,7 @@ type clroot =
         * string option (* root of replica in host fs *)
 
 (* Internal datatypes used in parsing command-line roots *)
-type protocol = File | Rsh | Socket | Ssh
+type protocol = File | Socket | Ssh
 type uri = protocol       (*   - a protocol *)
          * string option  (*   - an optional user *)
          * string option  (*   - an optional host *)
@@ -77,15 +80,17 @@ let getProtocolSlashSlash s =
     let protocol =
       match protocolName with
         "file" -> File
-      | "rsh" -> Rsh
+      | "rsh" ->
+          raise (Invalid_argument
+                  (Printf.sprintf "protocol rsh has been deprecated, use ssh instead (optionally specifying a different sshcmd preference)"))
       | "socket" -> Socket
       | "ssh" -> Ssh
       | "unison" ->
           raise(Invalid_argument
-                  (Printf.sprintf "protocol unison has been deprecated, use file, ssh, rsh, or socket instead" ))
+                  (Printf.sprintf "protocol unison has been deprecated, use file, ssh, or socket instead" ))
       | _ ->
           raise(Invalid_argument
-                  (Printf.sprintf "unrecognized protocol %s" protocolName)) in
+                  (Printf.sprintf "\"%s\": unrecognized protocol %s" s protocolName)) in
     Some(protocol,remainder)
   else if Str.string_match slashSlashRegexp s 0
   then Some(File,String.sub s 2 (String.length s - 2))
@@ -93,10 +98,10 @@ let getProtocolSlashSlash s =
   then
     let matched = Str.matched_string s in
     match matched with
-      "file:" | "ssh:" | "rsh:" | "socket:" ->
+      "file:" | "ssh:" | "socket:" ->
         raise(Util.Fatal
                 (Printf.sprintf
-                   "ill-formed root specification %s (%s must be followed by //)"
+                   "ill-formed root specification \"%s\" (%s must be followed by //)"
                    s matched))
     | _ -> None
   else None
@@ -112,23 +117,17 @@ let getUser s =
     (Some beforeAt,afterAt)
   else (None,s)
 
-(*ipv6 support*)
-let hostWithBracketsRegexp = Str.regexp "\\[.*\\]"
-let hostRegexp = Str.regexp "[-_a-zA-Z0-9.]+"
+let ipv6Regexp = "[a-f0-9:.]+\\(%[-_a-zA-Z0-9~%.]+\\)?"
+(* Hostname, IP or Unix domain socket path *)
+let hostRegexp = Str.regexp ("[-_a-zA-Z0-9%.]+\\|{[^}]+}\\|\\[\\(" ^ ipv6Regexp ^ "\\)\\]")
 let getHost s =
-  if Str.string_match hostWithBracketsRegexp s 0
-  then
-    let host' = Str.matched_string s in
-    let s' = Str.string_after s (String.length host') in
-    let host = String.sub host' 1 ((String.length host')-2) in
-    (Some host,s')
-  else if Str.string_match hostRegexp s 0
+  if Str.string_match hostRegexp s 0
   then
     let host = Str.matched_string s in
+    let host' = try Str.matched_group 1 s with Not_found -> host in
     let s' = Str.string_after s (String.length host) in
-    (Some host,s')
-  else
-     (None,s)
+    (Some host', s')
+  else (None,s)
 
 let colonPortRegexp = Str.regexp ":[^/]+"
 let getPort s =
@@ -152,6 +151,7 @@ let getPort s =
    and path is guaranteed to be non-empty
 *)
 let parseUri s =
+  let s = Util.trimWhitespace s in
   match getProtocolSlashSlash s with
     None ->
       (File,None,None,None,Some s)
@@ -170,13 +170,21 @@ let parseUri s =
                   (Printf.sprintf "ill-formed root specification %s" s)) in
       (protocol,userOpt,hostOpt,portOpt,pathOpt)
 
+let parseHostPort s =
+  let (hostOpt, s1) = getHost s in
+  let (portOpt, s2) = getPort s1 in
+  if String.length s2 > 0 then
+    raise (Util.Transient
+              (Printf.sprintf "ill-formed host specification %s" s));
+  ((match hostOpt with Some h -> h | None -> ""), portOpt)
+
 (* These should succeed *)
 let t1 = "socket://tjim@saul.cis.upenn.edu:4040/hello/world"
 let t2 = "ssh://tjim@saul/hello/world"
-let t3 = "rsh://saul:4040/hello/world"
+(*let t3 = "rsh://saul:4040/hello/world"
 let t4 = "rsh://saul/hello/world"
 let t5 = "rsh://saul"
-let t6 = "rsh:///hello/world"
+let t6 = "rsh:///hello/world"*)
 let t7 = "///hello/world"
 let t8 = "//raptor/usr/local/bin"
 let t9 = "file://raptor/usr/local/bin"
@@ -189,11 +197,11 @@ let b2 = "RSH://saul/hello"
 let b3 = "rsh:/saul/hello"
 let b4 = "//s%aul/hello"
 
-let cannotAbbrevFileRx = Rx.rx "(file:|ssh:|rsh:|socket:).*"
+let cannotAbbrevFileRx = Rx.rx "(file:|ssh:|socket:).*"
 let networkNameRx = Rx.rx "//.*"
 (* Main external printing function *)
 let clroot2string = function
-  ConnectLocal None -> "."
+| ConnectLocal None | ConnectLocal (Some "") -> "."
 | ConnectLocal(Some s) ->
     if Rx.match_string cannotAbbrevFileRx s
     then if Rx.match_string networkNameRx s
@@ -201,38 +209,45 @@ let clroot2string = function
     else Printf.sprintf "file:///%s" s
     else s
 | ConnectBySocket(h,p,s) ->
-    Printf.sprintf "socket://%s:%s/%s" h p
+    let p = if p <> "" then ":" ^ p else p in
+    let h = if String.contains h ':' && h.[0] <> '{' then "[" ^ h ^ "]" else h in
+    Printf.sprintf "socket://%s%s/%s" h p
       (match s with None -> "" | Some x -> x)
 | ConnectByShell(sh,h,u,p,s) ->
     let user = match u with None -> "" | Some x -> x^"@" in
     let port = match p with None -> "" | Some x -> ":"^x in
     let path = match s with None -> "" | Some x -> x in
+    let h = if String.contains h ':' then "[" ^ h ^ "]" else h in
     Printf.sprintf "%s://%s%s%s/%s" sh user h port path
 
-let sshversion = Prefs.createString "sshversion" ""
-                "*optional version suffix for ssh command [1 or 2]"
-    ("This preference can be used to control which version "
-     ^ "of ssh should be used to connect to the server.  Legal values are "
-     ^ "1 and 2, which will cause unison to try to use \\verb|ssh1| or"
-     ^ "\\verb|ssh2| instead of just \\verb|ssh| to invoke ssh.  "
-     ^ "The default value is empty, which will make unison use whatever "
-     ^ "version of ssh is installed as the default `ssh' command.")
+(* Pref sshversion removed since 2.52 *)
+let () = Prefs.markRemoved "sshversion"
+
+let fixHost = function
+  | ConnectLocal _ as r -> r
+  | ConnectBySocket (h, "", s) ->
+      (match parseHostPort h with
+      | h, Some p -> ConnectBySocket (h, p, s)
+      | h, None -> ConnectBySocket (h, "", s))
+  | ConnectBySocket _ as r -> r
+  | ConnectByShell (sh, h, u, None, s) ->
+      let (h, p) = parseHostPort h in
+      ConnectByShell (sh, h, u, p, s)
+  | ConnectByShell _ as r -> r
 
 (* Main external function *)
 let parseRoot string =
   let illegal2 s = raise(Prefs.IllegalValue
                            (Printf.sprintf
-                              "%s: %s" string s)) in
+                              "\"%s\": %s" string s)) in
   let (protocol,user,host,port,path) = parseUri string in
   let clroot =
     match protocol,user,host,port with
     | _,_,None,Some _
     | _,Some _,None,None
-    | Rsh,_,None,_
+    | Socket, _, None, None
     | Ssh,_,None,_ ->
         illegal2 "missing host"
-    | Rsh,_,_,Some _ ->
-        illegal2 "ill-formed (cannot use a port number with rsh)"
     | File,_,_,Some _ ->
         illegal2 "ill-formed (cannot use a port number with file)"
     | File,_,Some h,None ->
@@ -242,14 +257,16 @@ let parseRoot string =
         | Some p -> ConnectLocal(Some(prefix^p)))
     | File,None,None,None ->
         ConnectLocal(path)
-    | Socket,None,Some h,Some p ->
+    | Socket, None, Some h, Some p when h.[0] <> '{' ->
         ConnectBySocket(h,p,path)
+    | Socket, None, Some h, None when h.[0] = '{' ->
+        ConnectBySocket (h, "", path)
     | Socket,Some _,_,_ ->
         illegal2 "ill-formed (cannot use a user with socket)"
     | Socket,_,_,None ->
         illegal2 "ill-formed (must give a port number with socket)"
-    | Rsh,_,Some h,_ ->
-        ConnectByShell("rsh",h,user,port,path)
+    | Socket, _, Some _, Some _ ->
+        illegal2 "ill-formed (must not give a port number with Unix domain socket)"
     | Ssh,_,Some h,_ ->
-        ConnectByShell("ssh"^(Prefs.read sshversion),h,user,port,path) in
+        ConnectByShell("ssh",h,user,port,path) in
   clroot
diff --git a/src/clroot.mli b/src/clroot.mli
index bd11540..db93eeb 100644
--- a/src/clroot.mli
+++ b/src/clroot.mli
@@ -6,7 +6,7 @@ type clroot =
     ConnectLocal of
           string option (* root *)
   | ConnectByShell of
-          string        (* shell = "rsh" or "ssh" *)
+          string        (* shell = "ssh" *)
         * string        (* name of host *)
         * string option (* user name to log in as *)
         * string option (* port *)
@@ -19,3 +19,7 @@ type clroot =
 val clroot2string : clroot -> string
 
 val parseRoot : string -> clroot
+
+(* Parse a clroot with manually constructed host
+   which may or may not include the port number *)
+val fixHost : clroot -> clroot
diff --git a/src/common.ml b/src/common.ml
index 17bbaf4..a3df301 100644
--- a/src/common.ml
+++ b/src/common.ml
@@ -68,16 +68,89 @@ let sortRoots rootList = Safelist.sort compareRoots rootList
 
 (* ---------------------------------------------------------------------- *)
 
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [Common.prevState]. It must
+   always remain exactly the same as the type [Common.prevState] in version
+   2.51.5. This means that if any of the types it is composed of changes then
+   for each changed type also a 2.51-compatible version must be created. *)
+type prevState251 =
+    Previous of Fileinfo.typ * Props.t251 * Os.fullfingerprint * Osx.ressStamp
+  | New
+
 type prevState =
     Previous of Fileinfo.typ * Props.t * Os.fullfingerprint * Osx.ressStamp
   | New
 
+let mprevState = Umarshal.(sum2
+                             (prod4 Fileinfo.mtyp Props.m Os.mfullfingerprint Osx.mressStamp id id)
+                             unit
+                             (function
+                              | Previous (a, b, c, d) -> I21 (a, b, c, d)
+                              | New -> I22 ())
+                             (function
+                              | I21 (a, b, c, d) -> Previous (a, b, c, d)
+                              | I22 () -> New))
+
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [Common.contentschange]. It
+   must always remain exactly the same as the type [Common.contentschange]
+   in version 2.51.5. This means that if any of the types it is composed of
+   changes then for each changed type also a 2.51-compatible version must be
+   created. *)
+type contentschange251 =
+    ContentsSame
+  | ContentsUpdated of Os.fullfingerprint * Fileinfo.stamp251 * Osx.ressStamp
+
 type contentschange =
     ContentsSame
   | ContentsUpdated of Os.fullfingerprint * Fileinfo.stamp * Osx.ressStamp
 
+let mcontentschange = Umarshal.(sum2 unit (prod3 Os.mfullfingerprint Fileinfo.mstamp Osx.mressStamp id id)
+                                  (function
+                                   | ContentsSame -> I21 ()
+                                   | ContentsUpdated (a, b, c) -> I22 (a, b, c))
+                                  (function
+                                   | I21 () -> ContentsSame
+                                   | I22 (a, b, c) -> ContentsUpdated (a, b, c)))
+
 type permchange     = PropsSame    | PropsUpdated
 
+let mpermchange = Umarshal.(sum2 unit unit
+                              (function
+                               | PropsSame -> I21 ()
+                               | PropsUpdated -> I22 ())
+                              (function
+                               | I21 () -> PropsSame
+                               | I22 () -> PropsUpdated))
+
+(* IMPORTANT!
+   These are the 2.51-compatible versions of types [Common.updateItem] and
+   [Common.updateContent]. They must always remain exactly the same as the
+   types [Common.updateItem] and [Common.updateContent] in version 2.51.5.
+   This means that if any of the types they are composed of changes then
+   for each changed type also a 2.51-compatible version must be created. *)
+type updateItem251 =
+    NoUpdates                         (* Path not changed *)
+  | Updates                           (* Path changed in this replica *)
+      of updateContent251             (*   - new state *)
+       * prevState251                 (*   - summary of old state *)
+  | Error                             (* Error while detecting updates *)
+      of string                       (*   - description of error *)
+
+and updateContent251 =
+    Absent                            (* Path refers to nothing *)
+  | File                              (* Path refers to an ordinary file *)
+      of Props.t251                   (*   - summary of current state *)
+       * contentschange251            (*   - hint to transport agent *)
+  | Dir                               (* Path refers to a directory *)
+      of Props.t251                   (*   - summary of current state *)
+       * (Name.t * updateItem251) list(*   - children;
+                                             MUST KEEP SORTED for recon *)
+       * permchange                   (*   - did permissions change? *)
+       * bool                         (*   - is the directory now empty? *)
+  | Symlink                           (* Path refers to a symbolic link *)
+      of string                       (*   - link text *)
+
 type updateItem =
     NoUpdates                         (* Path not changed *)
   | Updates                           (* Path changed in this replica *)
@@ -100,6 +173,99 @@ and updateContent =
   | Symlink                           (* Path refers to a symbolic link *)
       of string                       (*   - link text *)
 
+let mupdateItem_rec mupdateContent =
+  Umarshal.(sum3 unit (prod2 mupdateContent mprevState id id) string
+              (function
+               | NoUpdates -> I31 ()
+               | Updates (a, b) -> I32 (a, b)
+               | Error a -> I33 a)
+              (function
+               | I31 () -> NoUpdates
+               | I32 (a, b) -> Updates (a, b)
+               | I33 a -> Error a))
+
+let mupdateContent_rec mupdateItem =
+  Umarshal.(sum4
+              unit
+              (prod2 Props.m mcontentschange id id)
+              (prod4 Props.m (list (prod2 Name.m mupdateItem id id)) mpermchange bool id id)
+              string
+              (function
+               | Absent -> I41 ()
+               | File (a, b) -> I42 (a, b)
+               | Dir (a, b, c, d) -> I43 (a, b, c, d)
+               | Symlink a -> I44 a)
+              (function
+               | I41 () -> Absent
+               | I42 (a, b) -> File (a, b)
+               | I43 (a, b, c, d) -> Dir (a, b, c, d)
+               | I44 a -> Symlink a))
+
+let mupdateContent, mupdateItem =
+  Umarshal.rec2 mupdateItem_rec mupdateContent_rec
+
+(* Compatibility conversion functions *)
+
+let prev_to_compat251 (prev : prevState) : prevState251 =
+  match prev with
+  | Previous (typ, props, fp, ress) ->
+      Previous (typ, Props.to_compat251 props, fp, ress)
+  | New -> New
+
+let prev_of_compat251 (prev : prevState251) : prevState =
+  match prev with
+  | Previous (typ, props, fp, ress) ->
+      Previous (typ, Props.of_compat251 props, fp, ress)
+  | New -> New
+
+let change_to_compat251 (c : contentschange) : contentschange251 =
+  match c with
+  | ContentsSame -> ContentsSame
+  | ContentsUpdated (fp, stamp, ress) ->
+      ContentsUpdated (fp, Fileinfo.stamp_to_compat251 stamp, ress)
+
+let change_of_compat251 (c : contentschange251) : contentschange =
+  match c with
+  | ContentsSame -> ContentsSame
+  | ContentsUpdated (fp, stamp, ress) ->
+      ContentsUpdated (fp, Fileinfo.stamp_of_compat251 stamp, ress)
+
+let rec ui_to_compat251 (ui : updateItem) : updateItem251 =
+  match ui with
+  | NoUpdates -> NoUpdates
+  | Updates (uc, prev) -> Updates (uc_to_compat251 uc, prev_to_compat251 prev)
+  | Error s -> Error s
+
+and ui_of_compat251 (ui : updateItem251) : updateItem =
+  match ui with
+  | NoUpdates -> NoUpdates
+  | Updates (uc, prev) -> Updates (uc_of_compat251 uc, prev_of_compat251 prev)
+  | Error s -> Error s
+
+and children_to_compat251 l =
+  Safelist.map (fun (n, ui) -> (n, ui_to_compat251 ui)) l
+
+and children_of_compat251 l =
+  Safelist.map (fun (n, ui) -> (n, ui_of_compat251 ui)) l
+
+and uc_to_compat251 (uc : updateContent) : updateContent251 =
+  match uc with
+  | Absent -> Absent
+  | File (props, change) ->
+      File (Props.to_compat251 props, change_to_compat251 change)
+  | Dir (props, ch, perm, empty) ->
+      Dir (Props.to_compat251 props, children_to_compat251 ch, perm, empty)
+  | Symlink s -> Symlink s
+
+and uc_of_compat251 (uc : updateContent251) : updateContent =
+  match uc with
+  | Absent -> Absent
+  | File (props, change) ->
+      File (Props.of_compat251 props, change_of_compat251 change)
+  | Dir (props, ch, perm, empty) ->
+      Dir (Props.of_compat251 props, children_of_compat251 ch, perm, empty)
+  | Symlink s -> Symlink s
+
 (* ------------------------------------------------------------------------- *)
 
 type status =
diff --git a/src/common.mli b/src/common.mli
index a18bda3..8065ace 100644
--- a/src/common.mli
+++ b/src/common.mli
@@ -41,6 +41,39 @@ type 'a oneperpath = ONEPERPATH of 'a list
    filesystem below a given path and the state recorded in the archive below
    that path.  The other types are helpers. *)
 
+type prevState251 =
+    Previous of Fileinfo.typ * Props.t251 * Os.fullfingerprint * Osx.ressStamp
+  | New
+
+type contentschange251 =
+    ContentsSame
+  | ContentsUpdated of Os.fullfingerprint * Fileinfo.stamp251 * Osx.ressStamp
+type permchange = PropsSame | PropsUpdated
+
+(* Variable name prefix: "ui" *)
+type updateItem251 =
+    NoUpdates                         (* Path not changed *)
+  | Updates                           (* Path changed in this replica *)
+      of updateContent251             (*   - new state *)
+       * prevState251                 (*   - summary of old state *)
+  | Error                             (* Error while detecting updates *)
+      of string                       (*   - description of error *)
+
+(* Variable name prefix: "uc" *)
+and updateContent251 =
+    Absent                            (* Path refers to nothing *)
+  | File                              (* Path refers to an ordinary file *)
+      of Props.t251                   (*   - summary of current state *)
+       * contentschange251            (*   - hint to transport agent *)
+  | Dir                               (* Path refers to a directory *)
+      of Props.t251                   (*   - summary of current state *)
+       * (Name.t * updateItem251) list(*   - children
+                                             MUST KEEP SORTED for recon *)
+       * permchange                   (*   - did permissions change? *)
+       * bool                         (*   - is the directory now empty? *)
+  | Symlink                           (* Path refers to a symbolic link *)
+      of string                       (*   - link text *)
+
 type prevState =
     Previous of Fileinfo.typ * Props.t * Os.fullfingerprint * Osx.ressStamp
   | New
@@ -48,7 +81,6 @@ type prevState =
 type contentschange =
     ContentsSame
   | ContentsUpdated of Os.fullfingerprint * Fileinfo.stamp * Osx.ressStamp
-type permchange = PropsSame | PropsUpdated
 
 (* Variable name prefix: "ui" *)
 type updateItem =
@@ -74,6 +106,13 @@ and updateContent =
   | Symlink                           (* Path refers to a symbolic link *)
       of string                       (*   - link text *)
 
+val mupdateItem : updateItem Umarshal.t
+val mupdateContent : updateContent Umarshal.t
+
+val ui_to_compat251 : updateItem -> updateItem251
+val ui_of_compat251 : updateItem251 -> updateItem
+val uc_to_compat251 : updateContent -> updateContent251
+val uc_of_compat251 : updateContent251 -> updateContent
 
 (*****************************************************************************)
 (*            COMMON TYPES SHARED BY RECONCILER AND TRANSPORT AGENT          *)
diff --git a/src/compat402.ml b/src/compat402.ml
deleted file mode 100644
index 3bb56c6..0000000
--- a/src/compat402.ml
+++ /dev/null
@@ -1,40 +0,0 @@
-(* Functions added in OCaml 4.02 *)
-
-type bytes = string
-
-let output_bytes = output_string
-let output_substring = output
-
-module Bytes = struct
-  include String
-
-  let of_string x = x
-  let to_string x = x
-
-  let sub_string = sub
-end
-
-module Buffer = struct
-  include Buffer
-
-  let add_subbytes = add_substring
-end
-
-module Digest = struct
-  include Digest
-
-  let bytes = string
-  let subbytes = substring
-end
-
-module Marshal = struct
-  include Marshal
-
-  let from_bytes = from_string
-end
-
-module Unix = struct
-  include Unix
-
-  let write_substring = write
-end
diff --git a/src/compat403.ml b/src/compat403.ml
deleted file mode 100644
index 64713c7..0000000
--- a/src/compat403.ml
+++ /dev/null
@@ -1,20 +0,0 @@
-(* Functions added in OCaml 4.03 *)
-
-module String = struct
-  include String
-
-  let lowercase_ascii = lowercase
-  let capitalize_ascii = capitalize
-end
-
-module Unix = struct
-  include Unix
-
-  let has_symlink () = not Sys.win32
-end
-
-module Sys = struct
-  include Sys
-
-  let int_size = word_size - 1
-end
diff --git a/src/compat408.ml b/src/compat408.ml
deleted file mode 100644
index ead989d..0000000
--- a/src/compat408.ml
+++ /dev/null
@@ -1,102 +0,0 @@
-module Bytes = struct
-
-include Bytes
-
-(* The following code is taken from OCaml sources.
-   Authors of the code snippet: Alain Frisch and Daniel Bünzli *)
-
-(**************************************************************************)
-(*                                                                        *)
-(*                                 OCaml                                  *)
-(*                                                                        *)
-(*             Xavier Leroy, projet Cristal, INRIA Rocquencourt           *)
-(*                                                                        *)
-(*   Copyright 1996 Institut National de Recherche en Informatique et     *)
-(*     en Automatique.                                                    *)
-(*                                                                        *)
-(*   All rights reserved.  This file is distributed under the terms of    *)
-(*   the GNU Lesser General Public License version 2.1, with the          *)
-(*   special exception on linking described in the file LICENSE.          *)
-(*                                                                        *)
-(**************************************************************************)
-
-(** {6 Binary encoding/decoding of integers} *)
-
-external get_uint8 : bytes -> int -> int = "%string_safe_get"
-external get_uint16_ne : bytes -> int -> int = "%caml_string_get16"
-external get_int32_ne : bytes -> int -> int32 = "%caml_string_get32"
-external get_int64_ne : bytes -> int -> int64 = "%caml_string_get64"
-external set_int8 : bytes -> int -> int -> unit = "%string_safe_set"
-external set_int16_ne : bytes -> int -> int -> unit = "%caml_string_set16"
-external set_int32_ne : bytes -> int -> int32 -> unit = "%caml_string_set32"
-external set_int64_ne : bytes -> int -> int64 -> unit = "%caml_string_set64"
-external swap16 : int -> int = "%bswap16"
-external swap32 : int32 -> int32 = "%bswap_int32"
-external swap64 : int64 -> int64 = "%bswap_int64"
-
-let get_int8 b i =
-  ((get_uint8 b i) lsl (Sys.int_size - 8)) asr (Sys.int_size - 8)
-
-let get_uint16_le b i =
-  if Sys.big_endian then swap16 (get_uint16_ne b i)
-  else get_uint16_ne b i
-
-let get_uint16_be b i =
-  if not Sys.big_endian then swap16 (get_uint16_ne b i)
-  else get_uint16_ne b i
-
-let get_int16_ne b i =
-  ((get_uint16_ne b i) lsl (Sys.int_size - 16)) asr (Sys.int_size - 16)
-
-let get_int16_le b i =
-  ((get_uint16_le b i) lsl (Sys.int_size - 16)) asr (Sys.int_size - 16)
-
-let get_int16_be b i =
-  ((get_uint16_be b i) lsl (Sys.int_size - 16)) asr (Sys.int_size - 16)
-
-let get_int32_le b i =
-  if Sys.big_endian then swap32 (get_int32_ne b i)
-  else get_int32_ne b i
-
-let get_int32_be b i =
-  if not Sys.big_endian then swap32 (get_int32_ne b i)
-  else get_int32_ne b i
-
-let get_int64_le b i =
-  if Sys.big_endian then swap64 (get_int64_ne b i)
-  else get_int64_ne b i
-
-let get_int64_be b i =
-  if not Sys.big_endian then swap64 (get_int64_ne b i)
-  else get_int64_ne b i
-
-let set_int16_le b i x =
-  if Sys.big_endian then set_int16_ne b i (swap16 x)
-  else set_int16_ne b i x
-
-let set_int16_be b i x =
-  if not Sys.big_endian then set_int16_ne b i (swap16 x)
-  else set_int16_ne b i x
-
-let set_int32_le b i x =
-  if Sys.big_endian then set_int32_ne b i (swap32 x)
-  else set_int32_ne b i x
-
-let set_int32_be b i x =
-  if not Sys.big_endian then set_int32_ne b i (swap32 x)
-  else set_int32_ne b i x
-
-let set_int64_le b i x =
-  if Sys.big_endian then set_int64_ne b i (swap64 x)
-  else set_int64_ne b i x
-
-let set_int64_be b i x =
-  if not Sys.big_endian then set_int64_ne b i (swap64 x)
-  else set_int64_ne b i x
-
-let set_uint8 = set_int8
-let set_uint16_ne = set_int16_ne
-let set_uint16_be = set_int16_be
-let set_uint16_le = set_int16_le
-
-end
diff --git a/src/compat408.mli b/src/compat408.mli
deleted file mode 100644
index 64a4eb2..0000000
--- a/src/compat408.mli
+++ /dev/null
@@ -1,214 +0,0 @@
-module Bytes : sig
-
-include module type of Bytes
-
-(* The following code is taken from OCaml sources.
-   Authors of the code snippet: Alain Frisch and Daniel Bünzli *)
-
-(**************************************************************************)
-(*                                                                        *)
-(*                                 OCaml                                  *)
-(*                                                                        *)
-(*             Xavier Leroy, projet Cristal, INRIA Rocquencourt           *)
-(*                                                                        *)
-(*   Copyright 1996 Institut National de Recherche en Informatique et     *)
-(*     en Automatique.                                                    *)
-(*                                                                        *)
-(*   All rights reserved.  This file is distributed under the terms of    *)
-(*   the GNU Lesser General Public License version 2.1, with the          *)
-(*   special exception on linking described in the file LICENSE.          *)
-(*                                                                        *)
-(**************************************************************************)
-
-(** {1 Binary encoding/decoding of integers} *)
-
-(** The functions in this section binary encode and decode integers to
-    and from byte sequences.
-    All following functions raise [Invalid_argument] if the space
-    needed at index [i] to decode or encode the integer is not
-    available.
-    Little-endian (resp. big-endian) encoding means that least
-    (resp. most) significant bytes are stored first.  Big-endian is
-    also known as network byte order.  Native-endian encoding is
-    either little-endian or big-endian depending on {!Sys.big_endian}.
-    32-bit and 64-bit integers are represented by the [int32] and
-    [int64] types, which can be interpreted either as signed or
-    unsigned numbers.
-    8-bit and 16-bit integers are represented by the [int] type,
-    which has more bits than the binary encoding.  These extra bits
-    are handled as follows: {ul
-    {- Functions that decode signed (resp. unsigned) 8-bit or 16-bit
-    integers represented by [int] values sign-extend
-    (resp. zero-extend) their result.}
-    {- Functions that encode 8-bit or 16-bit integers represented by
-    [int] values truncate their input to their least significant
-    bytes.}}
-*)
-
-val get_uint8 : bytes -> int -> int
-(** [get_uint8 b i] is [b]'s unsigned 8-bit integer starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int8 : bytes -> int -> int
-(** [get_int8 b i] is [b]'s signed 8-bit integer starting at byte index [i].
-    @since 4.08
-*)
-
-val get_uint16_ne : bytes -> int -> int
-(** [get_uint16_ne b i] is [b]'s native-endian unsigned 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_uint16_be : bytes -> int -> int
-(** [get_uint16_be b i] is [b]'s big-endian unsigned 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_uint16_le : bytes -> int -> int
-(** [get_uint16_le b i] is [b]'s little-endian unsigned 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int16_ne : bytes -> int -> int
-(** [get_int16_ne b i] is [b]'s native-endian signed 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int16_be : bytes -> int -> int
-(** [get_int16_be b i] is [b]'s big-endian signed 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int16_le : bytes -> int -> int
-(** [get_int16_le b i] is [b]'s little-endian signed 16-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int32_ne : bytes -> int -> int32
-(** [get_int32_ne b i] is [b]'s native-endian 32-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int32_be : bytes -> int -> int32
-(** [get_int32_be b i] is [b]'s big-endian 32-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int32_le : bytes -> int -> int32
-(** [get_int32_le b i] is [b]'s little-endian 32-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int64_ne : bytes -> int -> int64
-(** [get_int64_ne b i] is [b]'s native-endian 64-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int64_be : bytes -> int -> int64
-(** [get_int64_be b i] is [b]'s big-endian 64-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val get_int64_le : bytes -> int -> int64
-(** [get_int64_le b i] is [b]'s little-endian 64-bit integer
-    starting at byte index [i].
-    @since 4.08
-*)
-
-val set_uint8 : bytes -> int -> int -> unit
-(** [set_uint8 b i v] sets [b]'s unsigned 8-bit integer starting at byte index
-    [i] to [v].
-    @since 4.08
-*)
-
-val set_int8 : bytes -> int -> int -> unit
-(** [set_int8 b i v] sets [b]'s signed 8-bit integer starting at byte index
-    [i] to [v].
-    @since 4.08
-*)
-
-val set_uint16_ne : bytes -> int -> int -> unit
-(** [set_uint16_ne b i v] sets [b]'s native-endian unsigned 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_uint16_be : bytes -> int -> int -> unit
-(** [set_uint16_be b i v] sets [b]'s big-endian unsigned 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_uint16_le : bytes -> int -> int -> unit
-(** [set_uint16_le b i v] sets [b]'s little-endian unsigned 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int16_ne : bytes -> int -> int -> unit
-(** [set_int16_ne b i v] sets [b]'s native-endian signed 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int16_be : bytes -> int -> int -> unit
-(** [set_int16_be b i v] sets [b]'s big-endian signed 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int16_le : bytes -> int -> int -> unit
-(** [set_int16_le b i v] sets [b]'s little-endian signed 16-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int32_ne : bytes -> int -> int32 -> unit
-(** [set_int32_ne b i v] sets [b]'s native-endian 32-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int32_be : bytes -> int -> int32 -> unit
-(** [set_int32_be b i v] sets [b]'s big-endian 32-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int32_le : bytes -> int -> int32 -> unit
-(** [set_int32_le b i v] sets [b]'s little-endian 32-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int64_ne : bytes -> int -> int64 -> unit
-(** [set_int64_ne b i v] sets [b]'s native-endian 64-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int64_be : bytes -> int -> int64 -> unit
-(** [set_int64_be b i v] sets [b]'s big-endian 64-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-val set_int64_le : bytes -> int -> int64 -> unit
-(** [set_int64_le b i v] sets [b]'s little-endian 64-bit integer
-    starting at byte index [i] to [v].
-    @since 4.08
-*)
-
-end
diff --git a/src/configure.ml b/src/configure.ml
deleted file mode 100644
index f4701dd..0000000
--- a/src/configure.ml
+++ /dev/null
@@ -1,55 +0,0 @@
-let (major, minor, patch) =
-  Scanf.sscanf Sys.ocaml_version "%d.%d.%d" (fun x y z -> (x, y, z))
-
-let compat4pred v = major < 4 || major = 4 && minor < v
-
-let compat4 =
-  [
-    compat4pred 2, "Compat402", "compat402.cmo";
-    compat4pred 3, "Compat403", "compat403.cmo";
-    compat4pred 8, "Compat408", "compat408.cmo" ]
-
-let objects =
-  List.fold_left (fun acc (p, _, n) -> if p then acc ^ " " ^ n else acc) ""
-
-let (flags, objs) =
-  match objects compat4 with
-  | "" -> ("", "")
-  | objs -> ("-pp \"ocaml " ^ Sys.argv.(0)  ^ " pp\"", objs)
-
-(* Compat for OCaml < 4.02 *)
-external string_create : int -> string = "caml_create_string"
-external input : in_channel -> string -> int -> int -> int = "caml_ml_input"
-external output : out_channel -> string -> int -> int -> unit = "caml_ml_output"
-
-let len = 65536
-
-let output_file name =
-  let () = set_binary_mode_out stdout true in
-  let f = open_in_bin name in
-  let s = string_create len in
-  let rec loop () =
-    match input f s 0 len with
-    | 0 -> close_in_noerr f
-    | l -> output stdout s 0 l; loop ()
-  in loop ()
-
-let same_file n1 n2 =
-  String.length n1 > 10 && String.sub n1 0 10 = String.sub n2 0 10
-
-let do_pp filen =
-  let fn = Filename.basename filen in
-  let stop = ref false in
-  List.iter (fun (c, n1, n2) -> if c && not !stop then
-    if same_file fn n2 then stop := true
-    else print_endline ("open " ^ n1)) compat4;
-  print_endline ("# 1 \"" ^ filen ^ "\" 1");
-  output_file filen
-
-let () =
-  if Array.length Sys.argv > 1 then begin
-    match Sys.argv.(1) with
-    | "objs" -> print_string objs
-    | "pp" -> do_pp Sys.argv.(2)
-    | _ -> ()
-  end else print_string flags
diff --git a/src/copy.ml b/src/copy.ml
index 60a0511..21e2274 100644
--- a/src/copy.ml
+++ b/src/copy.ml
@@ -24,19 +24,14 @@ let debug = Trace.debug "copy"
 let protect f g =
   try
     f ()
-  with Sys_error _ | Unix.Unix_error _ | Util.Transient _ as e ->
+  with e ->
     begin try g () with Sys_error _  | Unix.Unix_error _ -> () end;
     raise e
 
 let lwt_protect f g =
   Lwt.catch f
     (fun e ->
-       begin match e with
-         Sys_error _ | Unix.Unix_error _ | Util.Transient _ ->
-           begin try g () with Sys_error _  | Unix.Unix_error _ -> () end
-       | _ ->
-           ()
-       end;
+       begin try g () with Sys_error _  | Unix.Unix_error _ -> () end;
        Lwt.fail e)
 
 (****)
@@ -47,13 +42,14 @@ let lwt_protect f g =
 let checkForChangesToSourceLocal
       fspathFrom pathFrom archDesc archFp archStamp archRess newFpOpt paranoid =
   (* Retrieve attributes of current source file *)
-  let sourceInfo = Fileinfo.get true fspathFrom pathFrom in
+  let sourceInfo = Fileinfo.getBasicWithRess true fspathFrom pathFrom in
+  let sourceType = sourceInfo.Fileinfo.typ in
   match newFpOpt with
     None ->
       (* no newfp provided: so we need to compare the archive with the
          current source *)
       let clearlyChanged =
-           sourceInfo.Fileinfo.typ <> `FILE
+           sourceType <> `FILE
         || Props.length sourceInfo.Fileinfo.desc <> Props.length archDesc
         || Osx.ressLength sourceInfo.Fileinfo.osX.Osx.ressInfo <>
            Osx.ressLength archRess    in
@@ -63,7 +59,8 @@ let checkForChangesToSourceLocal
         && not (Fpcache.excelFile pathFrom)
         && match archStamp with
              Some (Fileinfo.InodeStamp inode) -> sourceInfo.Fileinfo.inode = inode
-           | Some (Fileinfo.CtimeStamp ctime) -> true
+           | Some (Fileinfo.NoStamp)          -> true
+           | Some (Fileinfo.RescanStamp)      -> false
            | None                             -> false   in
       let ressClearlyUnchanged =
            not clearlyChanged
@@ -71,7 +68,7 @@ let checkForChangesToSourceLocal
                              None dataClearlyUnchanged   in
       if dataClearlyUnchanged && ressClearlyUnchanged then begin
         if paranoid && not (Os.isPseudoFingerprint archFp) then begin
-          let newFp = Os.fingerprint fspathFrom pathFrom sourceInfo in
+          let newFp = Os.fingerprint fspathFrom pathFrom sourceType in
           if archFp <> newFp then begin
             Update.markPossiblyUpdated fspathFrom pathFrom;
             raise (Util.Transient (Printf.sprintf
@@ -84,7 +81,7 @@ let checkForChangesToSourceLocal
         end
       end else if
            clearlyChanged
-        || archFp <> Os.fingerprint fspathFrom pathFrom sourceInfo
+        || archFp <> Os.fingerprint fspathFrom pathFrom sourceType
       then
         raise (Util.Transient (Printf.sprintf
           "The source file %s\nhas been modified during synchronization.  \
@@ -95,17 +92,44 @@ let checkForChangesToSourceLocal
       assert (Os.isPseudoFingerprint archFp);
       (* ... so we can't compare the archive with the source; instead we
          need to compare the current source to the new fingerprint: *)
-      if newfp <> Os.fingerprint fspathFrom pathFrom sourceInfo then
+      if newfp <> Os.fingerprint fspathFrom pathFrom sourceType then
         raise (Util.Transient (Printf.sprintf
           "Current source file %s\n not same as transferred file.  \
            Transfer aborted."
           (Fspath.toPrintString (Fspath.concat fspathFrom pathFrom))))
 
+let mcheckForChangesToSource =
+  Umarshal.(prod2
+              (prod4 Path.mlocal Props.m Os.mfullfingerprint (option Fileinfo.mstamp) id id)
+              (prod3 Osx.mressStamp (option Os.mfullfingerprint) bool id id)
+              id id)
+
+let archStamp_to_compat251 = function
+  | Some stamp -> Some (Fileinfo.stamp_to_compat251 stamp)
+  | None -> None
+
+let archStamp_of_compat251 = function
+  | Some stamp -> Some (Fileinfo.stamp_of_compat251 stamp)
+  | None -> None
+
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspathFrom,
+          ((pathFrom, archDesc, archFp, archStamp), (archRess, newFpOpt, paranoid))) ->
+       (fspathFrom,
+          (pathFrom, Props.to_compat251 archDesc, archFp,
+            archStamp_to_compat251 archStamp, archRess, newFpOpt, paranoid)))
+  (fun (fspathFrom,
+          (pathFrom, archDesc, archFp, archStamp, archRess, newFpOpt, paranoid)) ->
+       (fspathFrom,
+          ((pathFrom, Props.of_compat251 archDesc, archFp,
+            archStamp_of_compat251 archStamp), (archRess, newFpOpt, paranoid))))
+
 let checkForChangesToSourceOnRoot =
   Remote.registerRootCmd
-    "checkForChangesToSource"
+    "checkForChangesToSource" ~convV0
+    mcheckForChangesToSource Umarshal.unit
     (fun (fspathFrom,
-          (pathFrom, archDesc, archFp, archStamp, archRess, newFpOpt, paranoid)) ->
+          ((pathFrom, archDesc, archFp, archStamp), (archRess, newFpOpt, paranoid))) ->
       checkForChangesToSourceLocal
         fspathFrom pathFrom archDesc archFp archStamp archRess newFpOpt paranoid;
       Lwt.return ())
@@ -113,13 +137,13 @@ let checkForChangesToSourceOnRoot =
 let checkForChangesToSource
       root pathFrom archDesc archFp archStamp archRess newFpOpt paranoid =
   checkForChangesToSourceOnRoot
-    root (pathFrom, archDesc, archFp, archStamp, archRess, newFpOpt, paranoid)
+    root ((pathFrom, archDesc, archFp, archStamp), (archRess, newFpOpt, paranoid))
 
 (****)
 
 let fileIsTransferred fspathTo pathTo desc fp ress =
-  let info = Fileinfo.get false fspathTo pathTo in
-  (info,
+  let info = Fileinfo.getBasicWithRess false fspathTo pathTo in
+  (Fileinfo.basic info,
    info.Fileinfo.typ = `FILE
      &&
    Props.length info.Fileinfo.desc = Props.length desc
@@ -127,7 +151,7 @@ let fileIsTransferred fspathTo pathTo desc fp ress =
    Osx.ressLength info.Fileinfo.osX.Osx.ressInfo =
    Osx.ressLength ress
      &&
-   let fp' = Os.fingerprint fspathTo pathTo info in
+   let fp' = Os.fingerprint fspathTo pathTo info.Fileinfo.typ in
    fp' = fp)
 
 (* We slice the files in 1GB chunks because that's the limit for
@@ -146,6 +170,8 @@ let rec fingerprintPrefix fspath path offset len accu =
 let fingerprintPrefixRemotely =
   Remote.registerServerCmd
     "fingerprintSubfile"
+    Umarshal.(prod3 Fspath.m Path.mlocal Uutil.Filesize.m id id)
+    Umarshal.(list Fingerprint.m)
     (fun _ (fspath, path, len) ->
        Lwt.return (fingerprintPrefix fspath path 0L len []))
 
@@ -168,11 +194,48 @@ let validFilePrefix connFrom fspathFrom pathFrom fspathTo pathTo info desc =
   end else
     Lwt.return None
 
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [transferStatus]. It must always
+   remain exactly the same as the type [transferStatus] in version 2.51.5. This
+   means that if any of the types it is composed of changes then for each
+   changed type also a 2.51-compatible version must be created. *)
+type transferStatus251 =
+    TransferSucceeded of Fileinfo.t251
+  | TransferNeedsDoubleCheckAgainstCurrentSource of Fileinfo.t251 * Os.fullfingerprint
+  | TransferFailed of string
+
 type transferStatus =
-    TransferSucceeded of Fileinfo.t
-  | TransferNeedsDoubleCheckAgainstCurrentSource of Fileinfo.t * Os.fullfingerprint
+    TransferSucceeded of Fileinfo.basic
+  | TransferNeedsDoubleCheckAgainstCurrentSource of Fileinfo.basic * Os.fullfingerprint
   | TransferFailed of string
 
+let mtransferStatus = Umarshal.(sum3
+                                  Fileinfo.mbasic
+                                  (prod2 Fileinfo.mbasic Os.mfullfingerprint id id)
+                                  string
+                                  (function
+                                   | TransferSucceeded a -> I31 a
+                                   | TransferNeedsDoubleCheckAgainstCurrentSource (a, b) -> I32 (a, b)
+                                   | TransferFailed a -> I33 a)
+                                  (function
+                                   | I31 a -> TransferSucceeded a
+                                   | I32 (a, b) -> TransferNeedsDoubleCheckAgainstCurrentSource (a, b)
+                                   | I33 a -> TransferFailed a))
+
+let transferStatus_to_compat251 (st : transferStatus) : transferStatus251 =
+  match st with
+  | TransferSucceeded info -> TransferSucceeded (Fileinfo.to_compat251 info)
+  | TransferNeedsDoubleCheckAgainstCurrentSource (info, fp) ->
+      TransferNeedsDoubleCheckAgainstCurrentSource (Fileinfo.to_compat251 info, fp)
+  | TransferFailed s -> TransferFailed s
+
+let transferStatus_of_compat251 (st : transferStatus251) : transferStatus =
+  match st with
+  | TransferSucceeded info -> TransferSucceeded (Fileinfo.of_compat251 info)
+  | TransferNeedsDoubleCheckAgainstCurrentSource (info, fp) ->
+      TransferNeedsDoubleCheckAgainstCurrentSource (Fileinfo.of_compat251 info, fp)
+  | TransferFailed s -> TransferFailed s
+
 (* Paranoid check: recompute the transferred file's fingerprint to match it
    with the archive's.  If the old
    fingerprint was a pseudo-fingerprint, we can't tell just from looking at the
@@ -182,8 +245,8 @@ type transferStatus =
    calculated the current source fingerprint.
  *)
 let paranoidCheck fspathTo pathTo realPathTo desc fp ress =
-  let info = Fileinfo.get false fspathTo pathTo in
-  let fp' = Os.fingerprint fspathTo pathTo info in
+  let info = Fileinfo.getBasic false fspathTo pathTo in
+  let fp' = Os.fingerprint fspathTo pathTo info.Fileinfo.typ in
   if Os.isPseudoFingerprint fp then begin
     Lwt.return (TransferNeedsDoubleCheckAgainstCurrentSource (info,fp'))
   end else if fp' <> fp then begin
@@ -218,7 +281,9 @@ let saveTempFileLocal (fspathTo, (pathTo, realPathTo, reason)) =
         (Fspath.toDebugString (Fspath.concat fspathTo savepath))))
 
 let saveTempFileOnRoot =
-  Remote.registerRootCmd "saveTempFile" saveTempFileLocal
+  Remote.registerRootCmd "saveTempFile"
+    Umarshal.(prod3 Path.mlocal Path.mlocal string id id) Umarshal.unit
+    saveTempFileLocal
 
 (****)
 
@@ -229,7 +294,18 @@ let removeOldTempFile fspathTo pathTo =
     Os.delete fspathTo pathTo
   end
 
-let openFileIn fspath path kind =
+(* There is an issue that not all threads are immediately cancelled when there
+   is a connection error. A waiting thread (in this case probably a thread in
+   one of the Lwt regions) may have been started and could open an fd but may
+   never be able to complete. [protect], [lwt_protect] and any other cleanup
+   code may never be triggered in this scenario because the thread just stops
+   (as eventually the connection cleanup kicks in and all threads are stopped).
+   As a hacky(?) solution, keep track of all open fds and close them when the
+   connection breaks. *)
+let inFdResource = Remote.resourceWithConnCleanup close_in close_in_noerr
+let outFdResource = Remote.resourceWithConnCleanup close_out close_out_noerr
+
+let openFileIn' fspath path kind =
   match kind with
     `DATA ->
       Fs.open_in_bin (Fspath.concat fspath path)
@@ -240,12 +316,19 @@ let openFileIn fspath path kind =
   | `RESS ->
       Osx.openRessIn fspath path
 
-let openFileOut fspath path kind len =
+let openFileIn fspath path kind =
+  inFdResource.register (openFileIn' fspath path kind)
+
+let closeFileIn = inFdResource.release
+
+let closeFileInNoErr = inFdResource.release_noerr
+
+let openFileOut' fspath path kind len =
   match kind with
     `DATA ->
       let fullpath = Fspath.concat fspath path in
-      let flags = [Unix.O_WRONLY;Unix.O_CREAT] in
-      let perm = 0o600 in
+      let flags = [Unix.O_WRONLY; Unix.O_CREAT; Unix.O_CLOEXEC] in
+      let perm = if Prefs.read Props.dontChmod then Props.perms Props.fileDefault else 0o600 in
       begin match Util.osType with
         `Win32 ->
           Fs.open_out_gen
@@ -265,7 +348,7 @@ let openFileOut fspath path kind len =
       end
   | `DATA_APPEND len ->
       let fullpath = Fspath.concat fspath path in
-      let perm = 0o600 in
+      let perm = if Prefs.read Props.dontChmod then Props.perms Props.fileDefault else 0o600 in
       let ch = Fs.open_out_gen [Open_wronly; Open_binary] perm fullpath in
       if not (Prefs.read Props.dontChmod) then Fs.chmod fullpath perm;
       LargeFile.seek_out ch (Uutil.Filesize.toInt64 len);
@@ -273,6 +356,13 @@ let openFileOut fspath path kind len =
   | `RESS ->
       Osx.openRessOut fspath path len
 
+let openFileOut fspath path kind len =
+  outFdResource.register (openFileOut' fspath path kind len)
+
+let closeFileOut = outFdResource.release
+
+let closeFileOutNoErr = outFdResource.release_noerr
+
 let setFileinfo fspathTo pathTo realPathTo update desc =
   match update with
     `Update _ -> Fileinfo.set fspathTo pathTo (`Copy realPathTo) desc
@@ -280,6 +370,57 @@ let setFileinfo fspathTo pathTo realPathTo update desc =
 
 (****)
 
+(* This unfortunate complexity is here to reduce network round-trips
+   and calls to [Update.translatePath], primarily in [Files.setProp]. *)
+let mxpath = Umarshal.(sum2 Path.mlocal Path.m)
+               (function `Local p -> I21 p | `Global p -> I22 p)
+               (function I21 p -> `Local p | I22 p -> `Global p)
+
+let loadPropsExtDataLocal (fspath, path, desc) =
+  let localPath = match path with
+    | `Local p -> p
+    | `Global p -> Update.translatePathLocal fspath p in
+  (Some localPath, Props.loadExtData fspath localPath desc)
+
+let loadPropsExtDataOnServer = Remote.registerServerCmd "propsExtData"
+  Umarshal.(prod3 Fspath.m mxpath Props.m id id)
+  Umarshal.(prod2 (option Path.mlocal) Props.mx id id)
+  (fun connFrom args -> Lwt.return (loadPropsExtDataLocal args))
+
+let propsWithExtDataLocal fspath path desc =
+  try (None, Props.withExtData desc)
+  with Not_found -> loadPropsExtDataLocal (fspath, path, desc)
+
+let propsWithExtDataConn connFrom fspath path desc =
+  try Lwt.return (None, Props.withExtData desc)
+  with Not_found -> loadPropsExtDataOnServer connFrom (fspath, path, desc)
+
+let propsExtDataOnRoot root path desc =
+  match root with
+  | (Common.Local, fspath) ->
+      Lwt.return (propsWithExtDataLocal fspath path desc)
+  | (Remote _, fspath) ->
+      propsWithExtDataConn (Remote.connectionOfRoot root) fspath path desc
+
+let propsWithExtData connFrom fspath path desc =
+  propsWithExtDataConn connFrom fspath (`Local path) desc >>= fun x ->
+  Lwt.return (snd x)
+
+let readPropsExtData root path desc =
+  propsExtDataOnRoot root (`Local path) desc >>= fun x ->
+  Lwt.return (snd x)
+
+let readPropsExtDataG root path desc =
+  propsExtDataOnRoot root (`Global path) desc
+
+(****)
+
+(* The fds opened in this function normally shouldn't be tracked for extra
+   cleanup at connection close because this is sequential non-Lwt code. Yet,
+   there is a risk that code called by [Uutil.showProgress] may include Lwt
+   code. For this reason only, it is better to include the fds in this
+   function in the fd cleanup scheme (done automatically by [openFile*] and
+   [closeFile*] functions). *)
 let copyContents fspathFrom pathFrom fspathTo pathTo fileKind fileLength ido =
   let use_id f = match ido with Some id -> f id | None -> () in
   let inFd = openFileIn fspathFrom pathFrom fileKind in
@@ -292,16 +433,16 @@ let copyContents fspathFrom pathFrom fspathTo pathTo fileKind fileLength ido =
               (fun l ->
                  use_id (fun id ->
 (* (Util.msg "Copied file %s (%d bytes)\n" (Path.toString pathFrom) l); *)
+                   if fileKind <> `RESS then Abort.checkAll ();
                    Uutil.showProgress id (Uutil.Filesize.ofInt l) "l"));
-            close_in inFd;
-            close_out outFd;
+            closeFileIn inFd;
+            closeFileOut outFd;
 (* ignore (Sys.command ("ls -l " ^ (Fspath.toString (Fspath.concat fspathTo pathTo)))) *)
          )
-         (fun () -> close_out_noerr outFd))
-    (fun () -> close_in_noerr inFd)
+         (fun () -> closeFileOutNoErr outFd))
+    (fun () -> closeFileInNoErr inFd)
 
-let localFile
-     fspathFrom pathFrom fspathTo pathTo realPathTo update desc ressLength ido =
+let localFileContents fspathFrom pathFrom fspathTo pathTo desc ressLength ido =
   Util.convertUnixErrorsToTransient
     "copying locally"
     (fun () ->
@@ -314,19 +455,26 @@ let localFile
         fspathFrom pathFrom fspathTo pathTo `DATA (Props.length desc) ido;
       if ressLength > Uutil.Filesize.zero then
         copyContents
-          fspathFrom pathFrom fspathTo pathTo `RESS ressLength ido;
-      setFileinfo fspathTo pathTo realPathTo update desc)
+          fspathFrom pathFrom fspathTo pathTo `RESS ressLength ido)
+
+let localFile
+     fspathFrom pathFrom fspathTo pathTo realPathTo update desc ressLength ido =
+  Util.convertUnixErrorsToTransient "copying locally" (fun () ->
+    localFileContents fspathFrom pathFrom fspathTo pathTo desc ressLength ido;
+    let (_, desc) = propsWithExtDataLocal fspathFrom (`Local pathFrom) desc in
+    setFileinfo fspathTo pathTo realPathTo update desc)
 
 (****)
 
-let tryCopyMovedFile fspathTo pathTo realPathTo update desc fp ress id =
-  if not (Prefs.read Xferhint.xferbycopying) then None else
+let tryCopyMovedFile connFrom fspathFrom pathFrom fspathTo pathTo realPathTo
+      update desc fp ress id =
+  if not (Prefs.read Xferhint.xferbycopying) then Lwt.return None else
   Util.convertUnixErrorsToTransient "tryCopyMovedFile" (fun() ->
     debug (fun () -> Util.msg "tryCopyMovedFile: -> %s /%s/\n"
       (Path.toString pathTo) (Os.fullfingerprint_to_string fp));
     match Xferhint.lookup fp with
       None ->
-        None
+        Lwt.return None
     | Some (candidateFspath, candidatePath, hintHandle) ->
         debug (fun () ->
           Util.msg
@@ -338,14 +486,15 @@ let tryCopyMovedFile fspathTo pathTo realPathTo update desc fp ress id =
              [true] is correct.  Otherwise, we don't expect to point
              to a symlink, and therefore we still get the correct
              result. *)
-          let info = Fileinfo.get true candidateFspath candidatePath in
+          let info = Fileinfo.getBasic true candidateFspath candidatePath in
           if
             info.Fileinfo.typ <> `ABSENT &&
             Props.length info.Fileinfo.desc = Props.length desc
           then begin
-            localFile
-              candidateFspath candidatePath fspathTo pathTo realPathTo
-              update desc (Osx.ressLength ress) (Some id);
+            localFileContents candidateFspath candidatePath fspathTo pathTo desc
+              (Osx.ressLength ress) (Some id);
+            propsWithExtData connFrom fspathFrom pathFrom desc >>= fun desc ->
+            setFileinfo fspathTo pathTo realPathTo update desc;
             let (info, isTransferred) =
               fileIsTransferred fspathTo pathTo desc fp ress in
             if isTransferred then begin
@@ -358,29 +507,29 @@ let tryCopyMovedFile fspathTo pathTo realPathTo update desc fp ress id =
                  (Fspath.toPrintString candidateFspath)
                  (Path.toString candidatePath)
               in
-              Some (info, msg)
+              Lwt.return (Some (info, msg))
             end else begin
               debug (fun () ->
                 Util.msg "tryCopyMoveFile: candidate file %s modified!\n"
                   (Path.toString candidatePath));
               Xferhint.deleteEntry hintHandle;
-              None
+              Lwt.return None
             end
           end else begin
             debug (fun () ->
               Util.msg "tryCopyMoveFile: candidate file %s disappeared!\n"
                 (Path.toString candidatePath));
             Xferhint.deleteEntry hintHandle;
-            None
+            Lwt.return None
           end
         with
           Util.Transient s ->
             debug (fun () ->
               Util.msg
-                "tryCopyMovedFile: local copy from %s didn't work [%s]"
+                "tryCopyMovedFile: local copy from %s didn't work [%s]\n"
                 (Path.toString candidatePath) s);
             Xferhint.deleteEntry hintHandle;
-            None)
+            Lwt.return None)
 
 (****)
 
@@ -391,7 +540,8 @@ let tryCopyMovedFile fspathTo pathTo realPathTo update desc fp ress id =
 
 let rsyncActivated =
   Prefs.createBool "rsync" true
-    "!activate the rsync transfer mode"
+    ~category:(`Advanced `Remote)
+    "activate the rsync transfer mode"
     ("Unison uses the 'rsync algorithm' for 'diffs-only' transfer "
      ^ "of updates to large files.  Setting this flag to false makes Unison "
      ^ "use whole-file transfers instead.  Under normal circumstances, "
@@ -401,17 +551,21 @@ let rsyncActivated =
 
 let decompressor = ref Remote.MsgIdMap.empty
 
+let resetDecompressorState () =
+  decompressor := Remote.MsgIdMap.empty
+let () = Remote.at_conn_close resetDecompressorState
+
 let processTransferInstruction conn (file_id, ti) =
   Util.convertUnixErrorsToTransient
     "processing a transfer instruction"
     (fun () ->
-       ignore (Remote.MsgIdMap.find file_id !decompressor ti))
+       ignore ((fst (Remote.MsgIdMap.find file_id !decompressor)) ti))
 
 let marshalTransferInstruction =
-  (fun (file_id, (data, pos, len)) rem ->
+  (fun _ (file_id, (data, pos, len)) rem ->
      (Remote.encodeInt file_id :: (data, pos, len) :: rem,
       len + Remote.intSize)),
-  (fun buf pos ->
+  (fun _ buf pos ->
      let len = Bytearray.length buf - pos - Remote.intSize in
      (Remote.decodeInt buf pos, (buf, pos + Remote.intSize, len)))
 
@@ -426,7 +580,7 @@ let showPrefixProgress id kind =
   | _                -> ()
 
 let compress conn
-     (biOpt, fspathFrom, pathFrom, fileKind, sizeFrom, id, file_id) =
+     ((biOpt, fspathFrom, pathFrom, fileKind), (sizeFrom, id, file_id)) =
   Lwt.catch
     (fun () ->
        streamTransferInstruction conn
@@ -439,6 +593,7 @@ let compress conn
               (fun () ->
                  showPrefixProgress id fileKind;
                  let showProgress count =
+                   if fileKind <> `RESS then Abort.checkAll ();
                    Uutil.showProgress id (Uutil.Filesize.ofInt count) "r" in
                  let compr =
                    match biOpt with
@@ -451,38 +606,60 @@ let compress conn
                  compr
                    (fun ti -> processTransferInstructionRemotely (file_id, ti))
                        >>= fun () ->
-                 close_in infd;
+                 closeFileIn infd;
                  Lwt.return ())
               (fun () ->
-                 close_in_noerr infd)))
+                 closeFileInNoErr infd)))
     (fun e ->
        (* We cannot wrap the code above with the handler below,
           as the code is executed asynchronously. *)
        Util.convertUnixErrorsToTransient "transferring file contents"
          (fun () -> raise e))
 
-let compressRemotely = Remote.registerServerCmd "compress" compress
+let mdata = Umarshal.(sum3 unit Uutil.Filesize.m unit
+                        (function
+                         | `DATA -> I31 ()
+                         | `DATA_APPEND a -> I32 a
+                         | `RESS -> I33 ())
+                        (function
+                         | I31 () -> `DATA
+                         | I32 a -> `DATA_APPEND a
+                         | I33 () -> `RESS))
+
+let mcompress = Umarshal.(prod2
+                            (prod4 (option Transfer.Rsync.mrsync_block_info) Fspath.m Path.mlocal mdata id id)
+                            (prod3 Uutil.Filesize.m Uutil.File.m int id id)
+                            id id)
+
+let convV0 = Remote.makeConvV0FunArg
+  (fun ((biOpt, fspathFrom, pathFrom, fileKind), (sizeFrom, id, file_id)) ->
+       (biOpt, fspathFrom, pathFrom, fileKind, sizeFrom, id, file_id))
+  (fun (biOpt, fspathFrom, pathFrom, fileKind, sizeFrom, id, file_id) ->
+       ((biOpt, fspathFrom, pathFrom, fileKind), (sizeFrom, id, file_id)))
+
+let compressRemotely =
+  Remote.registerServerCmd "compress" ~convV0 mcompress Umarshal.unit compress
 
 let close_all infd outfd =
   Util.convertUnixErrorsToTransient
     "closing files"
     (fun () ->
        begin match !infd with
-         Some fd -> close_in fd; infd := None
+         Some fd -> closeFileIn fd; infd := None
        | None    -> ()
        end;
        begin match !outfd with
-         Some fd -> close_out fd; outfd := None
+         Some fd -> closeFileOut fd; outfd := None
        | None    -> ()
        end)
 
 let close_all_no_error infd outfd =
   begin match !infd with
-    Some fd -> close_in_noerr fd
+    Some fd -> closeFileInNoErr fd
   | None    -> ()
   end;
   begin match !outfd with
-    Some fd -> close_out_noerr fd
+    Some fd -> closeFileOutNoErr fd
   | None    -> ()
   end
 
@@ -510,11 +687,12 @@ let referenceFd fspath path kind infd =
   | Some fd ->
       fd
 
-let rsyncReg = Lwt_util.make_region (40 * 1024)
+let rsyncReg = Remote.lwtRegionWithConnCleanup (40 * 1024)
+
 let rsyncThrottle useRsync srcFileSize destFileSize f =
   if not useRsync then f () else
   let l = Transfer.Rsync.memoryFootprint srcFileSize destFileSize in
-  Lwt_util.run_in_region rsyncReg l f
+  Lwt_util.run_in_region !rsyncReg l f
 
 let transferFileContents
       connFrom fspathFrom pathFrom fspathTo pathTo realPathTo update
@@ -525,6 +703,7 @@ let transferFileContents
   let outfd = ref None in
   let infd = ref None in
   let showProgress count =
+    if fileKind <> `RESS then Abort.checkAll ();
     Uutil.showProgress id (Uutil.Filesize.ofInt count) "r" in
 
   let destFileSize =
@@ -554,7 +733,7 @@ let transferFileContents
                protect
                  (fun () -> Transfer.Rsync.rsyncPreprocess
                               ifd srcFileSize destFileSize)
-                 (fun () -> close_in_noerr ifd)
+                 (fun () -> closeFileInNoErr ifd)
              in
              close_all infd outfd;
              (Some bi,
@@ -581,9 +760,9 @@ let transferFileContents
     Lwt.catch
       (fun () ->
          debug (fun () -> Util.msg "Starting the actual transfer\n");
-         decompressor := Remote.MsgIdMap.add file_id decompr !decompressor;
+         decompressor := Remote.MsgIdMap.add file_id (decompr, (infd, outfd)) !decompressor;
          compressRemotely connFrom
-           (bi, fspathFrom, pathFrom, fileKind, srcFileSize, id, file_id)
+           ((bi, fspathFrom, pathFrom, fileKind), (srcFileSize, id, file_id))
            >>= fun () ->
          decompressor :=
            Remote.MsgIdMap.remove file_id !decompressor; (* For GC *)
@@ -614,6 +793,7 @@ let transferResourceForkAndSetFileinfo
   end else
     Lwt.return ()
   end >>= fun () ->
+  propsWithExtData connFrom fspathFrom pathFrom desc >>= fun desc ->
   setFileinfo fspathTo pathTo realPathTo update desc;
   debug (fun() -> Util.msg "Resource fork transferred for %s; doing last paranoid check\n"
     (Path.toString realPathTo));
@@ -650,6 +830,11 @@ let reallyTransferFile
 
 let filesBeingTransferred = Hashtbl.create 17
 
+let resetFileTransferState () =
+  (* The waiting threads should be collected by GC *)
+  Hashtbl.clear filesBeingTransferred
+let () = Remote.at_conn_close resetFileTransferState
+
 let wakeupNextTransfer fp =
   match
     try
@@ -692,7 +877,8 @@ let rec registerFileTransfer pathTo fp f =
 
 let copyprog =
   Prefs.createString "copyprog" "rsync --partial --inplace --compress"
-    "!external program for copying large files"
+    ~category:(`Advanced `General)
+    "external program for copying large files"
     ("A string giving the name of an "
      ^ "external program that can be used to copy large files efficiently  "
      ^ "(plus command-line switches telling it to copy files in-place).  "
@@ -702,7 +888,8 @@ let copyprog =
 let copyprogrest =
   Prefs.createString
     "copyprogrest" "rsync --partial --append-verify --compress"
-    "!variant of copyprog for resuming partial transfers"
+    ~category:(`Advanced `General)
+    "variant of copyprog for resuming partial transfers"
     ("A variant of {\\tt copyprog} that names an external program "
      ^ "that should be used to continue the transfer of a large file "
      ^ "that has already been partially transferred.  Typically, "
@@ -713,7 +900,8 @@ let copyprogrest =
 
 let copythreshold =
   Prefs.createInt "copythreshold" (-1)
-    "!use copyprog on files bigger than this (if >=0, in Kb)"
+    ~category:(`Advanced `General)
+    "use copyprog on files bigger than this (if >=0, in Kb)"
     ("A number indicating above what filesize (in kilobytes) Unison should "
      ^ "use the external "
      ^ "copying utility specified by {\\tt copyprog}. Specifying 0 will cause "
@@ -723,20 +911,13 @@ let copythreshold =
      ^ "See \\sectionref{speeding}{Making Unison Faster on Large Files} "
      ^ "for more information.")
 
-let copyquoterem =
-  Prefs.createBoolWithDefault "copyquoterem"
-    "!add quotes to remote file name for copyprog (true/false/default)"
-    ("When set to {\\tt true}, this flag causes Unison to add an extra layer "
-     ^ "of quotes to the remote path passed to the external copy program. "
-     ^ "This is needed by rsync, for example, which internally uses an ssh "
-     ^ "connection requiring an extra level of quoting for paths containing "
-     ^ "spaces. When this flag is set to {\\tt default}, extra quotes are "
-     ^ "added if the value of {\\tt copyprog} contains the string "
-     ^ "{\\tt rsync}.")
+(* Pref copyquoterem removed since 2.53.3 *)
+let () = Prefs.markRemoved "copyquoterem"
 
 let copymax =
   Prefs.createInt "copymax" 1
-    "!maximum number of simultaneous copyprog transfers"
+    ~category:(`Advanced `General)
+    "maximum number of simultaneous copyprog transfers"
     ("A number indicating how many instances of the external copying utility \
       Unison is allowed to run simultaneously (default to 1).")
 
@@ -747,7 +928,7 @@ let formatConnectionInfo root =
       (* Find the (unique) nonlocal root *)
       match
          Safelist.find (function Clroot.ConnectLocal _ -> false | _ -> true)
-           (Safelist.map Clroot.parseRoot (Globals.rawRoots()))
+           (Globals.parsedClRawRoots ())
       with
         Clroot.ConnectByShell (_,rawhost,uo,_,_) ->
             (match uo with None -> "" | Some u -> u ^ "@")
@@ -771,7 +952,7 @@ let shouldUseExternalCopyprog update desc =
   && update = `Copy
 
 let prepareExternalTransfer fspathTo pathTo =
-  let info = Fileinfo.get false fspathTo pathTo in
+  let info = Fileinfo.getBasic false fspathTo pathTo in
   match info.Fileinfo.typ with
     `FILE when Props.length info.Fileinfo.desc > Uutil.Filesize.zero ->
       let perms = Props.perms info.Fileinfo.desc in
@@ -789,9 +970,9 @@ let prepareExternalTransfer fspathTo pathTo =
       false
 
 let finishExternalTransferLocal connFrom
-      (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
-       update, desc, fp, ress, id) =
-  let info = Fileinfo.get false fspathTo pathTo in
+      ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+       (update, desc, fp, ress, id)) =
+  let info = Fileinfo.getBasic false fspathTo pathTo in
   if
     info.Fileinfo.typ <> `FILE ||
     Props.length info.Fileinfo.desc <> Props.length desc
@@ -805,44 +986,61 @@ let finishExternalTransferLocal connFrom
   Xferhint.insertEntry fspathTo pathTo fp;
   Lwt.return res
 
+let convV0 = Remote.makeConvV0Funs
+  (fun ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+         (update, desc, fp, ress, id)) ->
+       (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
+         update, Props.to_compat251 desc, fp, ress, id))
+  (fun (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
+         update, desc, fp, ress, id) ->
+       ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+         (update, Props.of_compat251 desc, fp, ress, id)))
+  transferStatus_to_compat251
+  transferStatus_of_compat251
+
+let mcopyOrUpdate = Umarshal.(sum2 unit (prod2 Uutil.Filesize.m Uutil.Filesize.m id id)
+                                (function
+                                 | `Copy -> I21 ()
+                                 | `Update (a, b) -> I22 (a, b))
+                                (function
+                                 | I21 () -> `Copy
+                                 | I22 (a, b) -> `Update (a, b)))
+
+let mfinishExternalTransfer = Umarshal.(prod2
+                                          (prod5 Fspath.m Path.mlocal Fspath.m Path.mlocal Path.mlocal id id)
+                                          (prod5 mcopyOrUpdate Props.m Os.mfullfingerprint Osx.mressStamp Uutil.File.m id id)
+                                          id id)
+
 let finishExternalTransferOnRoot =
   Remote.registerRootCmdWithConnection
-    "finishExternalTransfer" finishExternalTransferLocal
+    "finishExternalTransfer" ~convV0
+    mfinishExternalTransfer mtransferStatus finishExternalTransferLocal
 
-let copyprogReg = Lwt_util.make_region 1
+let copyprogReg = Remote.lwtRegionWithConnCleanup 1
 
 let transferFileUsingExternalCopyprog
              rootFrom pathFrom rootTo fspathTo pathTo realPathTo
              update desc fp ress id useExistingTarget =
   Uutil.showProgress id Uutil.Filesize.zero "ext";
-  let prog =
+  let progWithArgs =
     if useExistingTarget then
       Prefs.read copyprogrest
     else
       Prefs.read copyprog
   in
-  let extraquotes = Prefs.read copyquoterem = `True
-                 || (  Prefs.read copyquoterem = `Default
-                    && Util.findsubstring "rsync" prog <> None) in
-  let addquotes root s =
-    match root with
-    | Common.Local, _ -> s
-    | Common.Remote _, _ -> if extraquotes then Uutil.quotes s else s in
   let fromSpec =
       (formatConnectionInfo rootFrom)
-    ^ (addquotes rootFrom
-         (Fspath.toString (Fspath.concat (snd rootFrom) pathFrom))) in
+    ^ (Fspath.toString (Fspath.concat (snd rootFrom) pathFrom)) in
   let toSpec =
       (formatConnectionInfo rootTo)
-    ^ (addquotes rootTo
-         (Fspath.toString (Fspath.concat fspathTo pathTo))) in
-  let cmd = prog ^ " "
-             ^ (Uutil.quotes fromSpec) ^ " "
-             ^ (Uutil.quotes toSpec) in
-  Trace.log (Printf.sprintf "%s\n" cmd);
-  Lwt_util.resize_region copyprogReg (Prefs.read copymax);
-  Lwt_util.run_in_region copyprogReg 1
-    (fun () -> External.runExternalProgram cmd) >>= fun (_, log) ->
+    ^ (Fspath.toString (Fspath.concat fspathTo pathTo)) in
+  Trace.log (progWithArgs ^ " " ^ fromSpec ^ " " ^ toSpec ^ "\n");
+  Lwt_util.resize_region !copyprogReg (Prefs.read copymax);
+  let args = Str.split (Str.regexp "[ \t]+") progWithArgs in
+  let prog = match args with [] -> assert false | h :: _ -> h in
+  Lwt_util.run_in_region !copyprogReg 1
+    (fun () -> External.runExternalProgramArgs prog
+                 (Array.of_list (args @ [fromSpec; toSpec]))) >>= fun (_, log) ->
   debug (fun() ->
            let l = Util.trimWhitespace log in
            Util.msg "transferFileUsingExternalCopyprog %s: returned...\n%s%s"
@@ -850,14 +1048,14 @@ let transferFileUsingExternalCopyprog
              l (if l="" then "" else "\n"));
   Uutil.showProgress id (Props.length desc) "ext";
   finishExternalTransferOnRoot rootTo rootFrom
-    (snd rootFrom, pathFrom, fspathTo, pathTo, realPathTo,
-     update, desc, fp, ress, id)
+    ((snd rootFrom, pathFrom, fspathTo, pathTo, realPathTo),
+     (update, desc, fp, ress, id))
 
 (****)
 
 let transferFileLocal connFrom
-      (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
-       update, desc, fp, ress, id) =
+      ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+       (update, desc, fp, ress, id)) =
   let (tempInfo, isTransferred) =
     fileIsTransferred fspathTo pathTo desc fp ress in
   if isTransferred then begin
@@ -869,16 +1067,16 @@ let transferFileLocal connFrom
         (Fspath.toDebugString fspathTo) (Path.toString realPathTo) in
     let len = Uutil.Filesize.add (Props.length desc) (Osx.ressLength ress) in
     Uutil.showProgress id len "alr";
+    propsWithExtData connFrom fspathFrom pathFrom desc >>= fun desc ->
     setFileinfo fspathTo pathTo realPathTo update desc;
     Xferhint.insertEntry fspathTo pathTo fp;
     Lwt.return (`DONE (TransferSucceeded tempInfo, Some msg))
   end else
     registerFileTransfer pathTo fp
       (fun () ->
-         match
-           tryCopyMovedFile fspathTo pathTo realPathTo update desc fp ress id
-         with
-           Some (info, msg) ->
+         tryCopyMovedFile connFrom fspathFrom pathFrom
+           fspathTo pathTo realPathTo update desc fp ress id >>= function
+         | Some (info, msg) ->
              (* Transfer was performed by copying *)
              Xferhint.insertEntry fspathTo pathTo fp;
              Lwt.return (`DONE (TransferSucceeded info, Some msg))
@@ -894,12 +1092,37 @@ let transferFileLocal connFrom
                Lwt.return (`DONE (status, None))
              end)
 
+let convV0 = Remote.makeConvV0Funs
+  (fun ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+         (update, desc, fp, ress, id)) ->
+       (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
+         update, Props.to_compat251 desc, fp, ress, id))
+  (fun (fspathFrom, pathFrom, fspathTo, pathTo, realPathTo,
+         update, desc, fp, ress, id) ->
+       ((fspathFrom, pathFrom, fspathTo, pathTo, realPathTo),
+         (update, Props.of_compat251 desc, fp, ress, id)))
+  (function
+   | `DONE (a, b) -> `DONE (transferStatus_to_compat251 a, b)
+   | `EXTERNAL a -> `EXTERNAL a)
+  (function
+   | `DONE (a, b) -> `DONE (transferStatus_of_compat251 a, b)
+   | `EXTERNAL a -> `EXTERNAL a)
+
+let mtransferFile = Umarshal.(sum2 (prod2 mtransferStatus (option string) id id) bool
+                                (function
+                                 | `DONE (a, b) -> I21 (a, b)
+                                 | `EXTERNAL a -> I22 a)
+                                (function
+                                 | I21 (a, b) -> `DONE (a, b)
+                                 | I22 a -> `EXTERNAL a))
+
 let transferFileOnRoot =
-  Remote.registerRootCmdWithConnection "transferFile" transferFileLocal
+  Remote.registerRootCmdWithConnection "transferFile" ~convV0
+    mfinishExternalTransfer mtransferFile transferFileLocal
 
 (* We limit the size of the output buffers to about 512 KB
    (we cannot go above the limit below plus 64) *)
-let transferFileReg = Lwt_util.make_region 440
+let transferFileReg = Remote.lwtRegionWithConnCleanup 440
 
 let bufferSize sz =
     (* Token queue *)
@@ -914,8 +1137,8 @@ let transferFile
   let f () =
     Abort.check id;
     transferFileOnRoot rootTo rootFrom
-      (snd rootFrom, pathFrom, fspathTo, pathTo, realPathTo,
-       update, desc, fp, ress, id) >>= fun status ->
+      ((snd rootFrom, pathFrom, fspathTo, pathTo, realPathTo),
+       (update, desc, fp, ress, id)) >>= fun status ->
     match status with
       `DONE (status, msg) ->
          begin match msg with
@@ -945,7 +1168,7 @@ let transferFile
     f ()
   else
     let bufSz = bufferSize (max (Props.length desc) (Osx.ressLength ress)) in
-    Lwt_util.run_in_region transferFileReg bufSz f
+    Lwt_util.run_in_region !transferFileReg bufSz f
 
 (****)
 
@@ -991,7 +1214,8 @@ let file rootFrom pathFrom rootTo fspathTo pathTo realPathTo
       checkForChangesToSource rootFrom pathFrom desc fp stamp ress None true
         >>= fun () ->
       (* This function never returns (it is supposed to fail) *)
-      saveTempFileOnRoot rootTo (pathTo, realPathTo, reason)
+      saveTempFileOnRoot rootTo (pathTo, realPathTo, reason) >>= fun () ->
+      assert false
 
 (****)
 
@@ -1015,7 +1239,7 @@ let recursively fspathFrom pathFrom fspathTo pathTo =
         debug (fun () -> Util.msg "  Copying directory %s / %s to %s / %s\n"
           (Fspath.toDebugString fspathFrom) (Path.toString pFrom)
           (Fspath.toDebugString fspathTo) (Path.toString pTo));
-        Os.createDir fspathTo pTo info.Fileinfo.desc;
+        Os.createDir fspathTo pTo (Props.perms info.Fileinfo.desc);
         let ch = Os.childrenOf fspathFrom pFrom in
         Safelist.iter
           (fun n -> copy (Path.child pFrom n) (Path.child pTo n)) ch
diff --git a/src/copy.mli b/src/copy.mli
index d9e7f2b..c5b8572 100644
--- a/src/copy.mli
+++ b/src/copy.mli
@@ -13,7 +13,7 @@ val file :
  -> Fileinfo.stamp option (* source file stamp, if available *)
  -> Osx.ressStamp       (* resource info of file *)
  -> Uutil.File.t        (* file's index in UI (for progress bars) *)
- -> Fileinfo.t Lwt.t    (* information regarding the transferred file *)
+ -> Fileinfo.basic Lwt.t (* information regarding the transferred file *)
 
 val localFile :
     Fspath.t             (* fspath of source *)
@@ -33,3 +33,16 @@ val recursively :
  -> Fspath.t             (* fspath of target *)
  -> Path.local           (* path of target *)
  -> unit
+
+val readPropsExtData :
+    Common.root          (* root of source *)
+ -> Path.local           (* path of source *)
+ -> Props.t              (* props of source *)
+ -> Props.x Lwt.t        (* props with all ext data included *)
+
+val readPropsExtDataG :
+    Common.root          (* root of source *)
+ -> Path.t               (* path of source *)
+ -> Props.t              (* props of source *)
+ -> (Path.local option * Props.x) Lwt.t (* props with all ext data included
+                                           and path translated to local path *)
diff --git a/src/dune b/src/dune
index e5cd45a..1968ba2 100644
--- a/src/dune
+++ b/src/dune
@@ -1,7 +1,7 @@
 (library
  (name unison_lib)
  (wrapped false)
- (modules :standard \ linktext linkgtk linkgtk2 uigtk2 uimacbridge uimacbridgenew test)
+ (modules :standard \ linktext linkgtk3 uigtk3 uimacbridge test)
  (modules_without_implementation ui)
  (flags :standard
         -w -3-6-9-10-26-27-32-34-35-38-39-50-52
@@ -9,7 +9,7 @@
         -no-strict-sequence)
  (foreign_stubs
   (language c)
-  (names bytearray_stubs osxsupport pty hash_compat))
+  (names bytearray_stubs osxsupport pty hash_compat props_xattr props_acl))
  (c_library_flags -lutil)
  (libraries str unix lwt_lib bigarray))
 
@@ -18,12 +18,14 @@
 (executable
  (name linktext)
  (public_name unison)
+ (package unison)
  (modules linktext)
  (libraries unison_lib))
 
 (executable
- (name linkgtk2)
- (public_name unison-gtk2)
+ (name linkgtk3)
+ (public_name unison-gui)
+ (package unison-gui)
  (flags :standard -w -3-6-9-27-32-52)
- (modules linkgtk2 uigtk2)
- (libraries threads unison_lib lablgtk2))
+ (modules linkgtk3 uigtk3)
+ (libraries threads unison_lib lablgtk3))
diff --git a/src/external.ml b/src/external.ml
index 71f8bbf..f133682 100644
--- a/src/external.ml
+++ b/src/external.ml
@@ -25,6 +25,58 @@ let debug = Util.debug "external"
 let (>>=) = Lwt.bind
 open Lwt
 
+(* For backwards compatibility with OCaml < 4.12 *)
+let path =
+  try
+    Str.split (Str.regexp (if Util.osType = `Win32 then ";" else ":"))
+      (Sys.getenv "PATH")
+  with Not_found ->
+    []
+
+let search_in_path ?(path = path) name =
+  if String.contains name '/' then name else
+  Filename.concat
+    (List.find (fun dir ->
+       let p = Filename.concat dir name in
+       let found = System.file_exists p in
+       debug (fun () -> Util.msg "'%s' ...%s\n" p
+         (match found with true -> "found" | false -> "not found"));
+       found)
+    path)
+    name
+
+(* Make sure external process resources are collected and zombie processes
+   reaped when the Lwt thread calling the external program is stopped
+   suddenly due to remote connection being closed. *)
+let close_process_noerr close pid x =
+  let pid = pid x in
+  begin try
+    Unix.kill pid (if Sys.os_type = "Win32" then Sys.sigkill else Sys.sigterm)
+    with Unix.Unix_error _ -> () end;
+  begin try ignore (Terminal.safe_waitpid pid) with Unix.Unix_error _ -> () end;
+  try ignore (close x) with Sys_error _ | Unix.Unix_error _ -> ()
+
+let inProcRes =
+  Remote.resourceWithConnCleanup System.close_process_in
+    (close_process_noerr System.close_process_in System.process_in_pid)
+let fullProcRes =
+  Remote.resourceWithConnCleanup System.close_process_full
+    (close_process_noerr System.close_process_full System.process_full_pid)
+
+let openProcessIn cmd = inProcRes.register (System.open_process_in cmd)
+let closeProcessIn = inProcRes.release
+
+(* Remove call to search_in_path once we require OCaml >= 4.12. *)
+let openProcessArgsIn cmd args = inProcRes.register (System.open_process_args_in (search_in_path cmd) args)
+let closeProcessArgsIn = inProcRes.release
+
+let openProcessFull cmd = fullProcRes.register (System.open_process_full cmd)
+let closeProcessFull = fullProcRes.release
+
+(* Remove call to search_in_path once we require OCaml >= 4.12. *)
+let openProcessArgsFull cmd args = fullProcRes.register (System.open_process_args_full (search_in_path cmd) args)
+let closeProcessArgsFull = fullProcRes.release
+
 let readChannelTillEof c =
   let lst = ref [] in
   let rec loop () =
@@ -56,12 +108,13 @@ let readChannelsTillEof l =
        >>= (fun res -> return (String.concat "\n" (Safelist.rev res))))
     l
 
-let runExternalProgram cmd =
+
+let runExternalProgramAux ~winProc ~posixProc =
   if Util.osType = `Win32 && not Util.isCygwin then begin
     debug (fun()-> Util.msg "Executing external program windows-style\n");
-    let c = System.open_process_in ("\"" ^ cmd ^ "\"") in
+    let c = winProc () in
     let log = Util.trimWhitespace (readChannelTillEof c) in
-    let returnValue = System.close_process_in c in
+    let returnValue = closeProcessIn c in
     let resultLog =
       (*cmd ^
       (if log <> "" then "\n\n" ^*) log (*else "")*) ^
@@ -71,12 +124,12 @@ let runExternalProgram cmd =
          "") in
     Lwt.return (returnValue, resultLog)
   end else
-    let (out, ipt, err) as desc = System.open_process_full cmd in
+    let (out, ipt, err) as desc = posixProc () in
     let out = Lwt_unix.intern_in_channel out in
     let err = Lwt_unix.intern_in_channel err in
     readChannelsTillEof [out;err]
     >>= (function [logOut;logErr] ->
-    let returnValue = System.close_process_full desc in
+    let returnValue = closeProcessFull desc in
     let logOut = Util.trimWhitespace logOut in
     let logErr = Util.trimWhitespace logErr in
     return (returnValue, (
@@ -90,3 +143,13 @@ let runExternalProgram cmd =
          else "\n\n" ^ Util.process_status_to_string returnValue)))
       (* Stop typechechecker from complaining about non-exhaustive pattern above *)
       | _ -> assert false)
+
+let runExternalProgram cmd =
+  runExternalProgramAux
+    ~winProc:(fun () -> openProcessIn ("\"" ^ cmd ^ "\""))
+    ~posixProc:(fun () -> openProcessFull cmd)
+
+let runExternalProgramArgs cmd args =
+  runExternalProgramAux
+    ~winProc:(fun () -> openProcessArgsIn cmd args)
+    ~posixProc:(fun () -> openProcessArgsFull cmd args)
diff --git a/src/external.mli b/src/external.mli
index 30d2dbd..d2d0bae 100644
--- a/src/external.mli
+++ b/src/external.mli
@@ -2,4 +2,5 @@
 (* Copyright 1999-2020, Benjamin C. Pierce (see COPYING for details) *)
 
 val runExternalProgram : string -> (Unix.process_status * string) Lwt.t
+val runExternalProgramArgs : string -> string array -> (Unix.process_status * string) Lwt.t
 val readChannelTillEof : in_channel -> string
diff --git a/src/features.ml b/src/features.ml
new file mode 100644
index 0000000..407561b
--- /dev/null
+++ b/src/features.ml
@@ -0,0 +1,85 @@
+(* Unison file synchronizer: src/features.ml *)
+(* Copyright 2021, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*)
+
+type id = string
+
+type t = { mutable enabled : bool;
+           arcFormatChange : bool;
+           validator : (id list -> bool -> string option) option }
+
+let allFeatures = Hashtbl.create 8
+let allNames = ref []
+
+let all () = !allNames
+
+let mem = List.mem
+
+let empty = []
+
+let changingArchiveFormat () =
+  let enabledArch name t accu =
+    if t.enabled && t.arcFormatChange then name :: accu else accu
+  in
+  Hashtbl.fold enabledArch allFeatures []
+
+let inter a b = List.filter (fun name -> mem name a) b
+
+let getEnabled () =
+  let enabled name t accu = if t.enabled then name :: accu else accu in
+  Hashtbl.fold enabled allFeatures []
+
+let setEnabled features =
+  Hashtbl.iter (fun name t -> t.enabled <- mem name features) allFeatures
+
+let resetEnabled () = setEnabled empty
+
+(***************)
+
+let validate features =
+  let aux name t =
+    let failed = match t.validator with
+      | Some fn -> fn features (mem name features)
+      | None -> None
+    in
+    match failed with
+    | None -> ()
+    | Some e ->
+        raise (Util.Fatal
+          ("Client and server are incompatible. Setting up feature \""
+           ^ name ^ "\" failed with error\n\"" ^ e ^ "\".\n\n"
+           ^ "It may be possible to rectify this by changing the user "
+           ^ "preferences.\nUltimately, it may require upgrading either "
+           ^ "the server or the client."))
+  in
+  Hashtbl.iter aux allFeatures
+
+let validateEnabled () = validate (getEnabled ())
+
+(***************)
+
+let enabled feature = feature.enabled
+
+let dummy = { enabled = false; arcFormatChange = false; validator = None }
+
+let register name ?(arcFormatChange = false) validatefn =
+  if Hashtbl.mem allFeatures name then
+    raise (Util.Fatal ("Feature " ^ name ^ " registered twice"));
+  let v = { enabled = false; arcFormatChange; validator = validatefn } in
+  Hashtbl.add allFeatures name v;
+  allNames := name :: !allNames;
+  v
+
diff --git a/src/features.mli b/src/features.mli
new file mode 100644
index 0000000..3001ab4
--- /dev/null
+++ b/src/features.mli
@@ -0,0 +1,87 @@
+(* Unison file synchronizer: src/features.mli *)
+(* Copyright 2021, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*)
+
+type t
+(** The type of a feature. *)
+
+type id = string
+(** The type of feature's identifier. Features are identified by their name. *)
+
+val enabled : t -> bool
+(** Test whether a feature is currently enabled (included in the current
+    set of enabled features).
+
+    Feature negotiation must have been completed to get the correct result. *)
+
+val register : string -> ?arcFormatChange:bool ->
+        (id list -> bool -> string option) option -> t
+(** [register n f] registers a supported feature with a unique identifier [n].
+
+    [f] is an optional validation function that will be called during feature
+    negotiation. [f] will receive as the first argument the feature set to be
+    enabled as a result of negotiation and as the second argument a boolean
+    indicating whether the tested feature is included in the negotiated set.
+    [f] must return [Some msg] if the negotiation result must be rejected with
+    the error message [msg], otherwise it must return [None].
+
+    [archFormatChange] is an optional argument which indicates whether the
+    feature, if enabled, changes the archive format that is stored on disk.
+    In other words, it indicates if the archive stored while this feature was
+    enabled requires the existence of this feature to be read back in.
+
+    @return feature value that can be tested by {!Features.enabled} function.
+    @raise {!Util.Fatal} if [n] is not unique. *)
+
+val dummy : t
+(** A feature value that will never be included in feature negotiation or
+    the set of enabled features. *)
+
+val all : unit -> id list
+(** Set of all supported features registered by {!Features.register}. *)
+
+val empty : id list
+(** Empty set of features. *)
+
+val changingArchiveFormat : unit -> id list
+(** Set of all currently enabled features that impact the on-disk archive
+    format. The same features must exist in order to read in the archive. *)
+
+val mem : id -> id list -> bool
+(** [mem n s] tests whether feature with id [n] belongs to feature set [s]. *)
+
+val inter : id list -> id list -> id list
+(** Feature set intersection. *)
+
+val validate : id list -> unit
+(** [validate s] calls validation functions associated with each registered
+    feature in arbitrary order, with only features in [s] considered enabled.
+
+    @raise {!Util.Fatal} at first failed validation. *)
+
+val getEnabled : unit -> id list
+(** Set of enabled features. *)
+
+val resetEnabled : unit -> unit
+(** Make the set of enabled features empty. Can be used to reset the results
+    of previous feature negotiation. *)
+
+val setEnabled : id list -> unit
+(** [setEnabled s] makes [s] the set of enabled features. *)
+
+val validateEnabled : unit -> unit
+(** Same as {!Features.validate} with the set of currently enabled features. *)
+
diff --git a/src/fileinfo.ml b/src/fileinfo.ml
index 4a39ce8..38de32b 100644
--- a/src/fileinfo.ml
+++ b/src/fileinfo.ml
@@ -20,18 +20,22 @@ let debugV = Util.debug "fileinfo+"
 
 let allowSymlinks =
   Prefs.createBoolWithDefault "links"
-    "!allow the synchronization of symbolic links (true/false/default)"
+    ~category:(`Advanced `Sync)
+    "allow the synchronization of symbolic links (true/false/default)"
     ("When set to {\\tt true}, this flag causes Unison to synchronize \
       symbolic links.  When the flag is set to {\\tt false}, symbolic \
-      links will result in an error during update detection.  \
+      links will be ignored during update detection.  \
       Ordinarily, when the flag is set to {\\tt default}, symbolic \
       links are synchronized except when one of the hosts is running \
       Windows.  On a Windows client, Unison makes an attempt to detect \
       if symbolic links are supported and allowed by user privileges.  \
-      You may have to get elevated privileges to create symbolic links.")
+      You may have to get elevated privileges to create symbolic links.  \
+      When the flag is set to {\\t default} and symbolic links can't be \
+      synchronized then an error is produced during update detection.")
 
 let symlinksAllowed =
   Prefs.createBool "links-aux" true
+    ~category:(`Internal `Pseudo)
     "*Pseudo-preference for internal use only" ""
 
 let init b =
@@ -42,13 +46,55 @@ let init b =
 
 type typ = [ `ABSENT | `FILE | `DIRECTORY | `SYMLINK ]
 
+let mtyp = Umarshal.(sum4 unit unit unit unit
+                       (function
+                        | `ABSENT -> I41 ()
+                        | `FILE -> I42 ()
+                        | `DIRECTORY -> I43 ()
+                        | `SYMLINK -> I44 ())
+                       (function
+                        | I41 () -> `ABSENT
+                        | I42 () -> `FILE
+                        | I43 () -> `DIRECTORY
+                        | I44 () -> `SYMLINK))
+
 let type2string = function
     `ABSENT    -> "nonexistent"
   | `FILE      -> "file"
   | `DIRECTORY -> "dir"
   | `SYMLINK   -> "symlink"
 
-type t = { typ : typ; inode : int; desc : Props.t; osX : Osx.info}
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [Fileinfo.t]. It must always
+   remain exactly the same as the type [Fileinfo.t] in version 2.51.5. This
+   means that if any of the types it is composed of changes then for each
+   changed type also a 2.51-compatible version must be created. *)
+type t251 = { typ : typ; inode : int; desc : Props.t251; osX : Osx.info}
+
+type ('a, 'b) info = { typ : typ; inode : int; desc : 'a; osX : Osx.info }
+     constraint 'a = _ Props.props
+type t = (Props.t, [`WithRess]) info
+type basic = (Props.basic, [`NoRess]) info
+type bress = (Props.basic, [`WithRess]) info
+
+let minfo propsm = Umarshal.(prod4 mtyp int propsm Osx.minfo
+                    (fun {typ; inode; desc; osX} -> typ, inode, desc, osX)
+                    (fun (typ, inode, desc, osX) -> {typ; inode; desc; osX}))
+
+let m = minfo Props.m
+let mbasic = minfo Props.mbasic
+
+let to_compat251 (x : basic) : t251 =
+  { typ = x.typ;
+    inode = x.inode;
+    desc = Props.to_compat251 x.desc;
+    osX = x.osX }
+
+let of_compat251 (x : t251) : basic =
+  { typ = x.typ;
+    inode = x.inode;
+    desc = Props.of_compat251 x.desc;
+    osX = x.osX }
 
 (* Stat function that pays attention to pref for following links             *)
 let statFn fromRoot fspath path =
@@ -67,7 +113,21 @@ let statFn fromRoot fspath path =
   end else
     stats
 
-let get fromRoot fspath path =
+(* Warning! Do not change this string without some backwards compatibility
+   code in place. This string is not only meant for humans, it is also
+   processed by code. *)
+let symlinkErr = " is a symbolic link"
+let symlinkErrLen = String.length symlinkErr
+
+let shouldIgnore s =
+  Prefs.read allowSymlinks = `False &&
+    let l = String.length s in
+    if l > symlinkErrLen then
+      String.sub s (l - symlinkErrLen) symlinkErrLen = symlinkErr
+    else
+      false
+
+let getAux fromRoot fspath path getProps =
   Util.convertUnixErrorsToTransient
   "querying file information"
     (fun () ->
@@ -87,8 +147,9 @@ let get fromRoot fspath path =
                else
                  raise
                    (Util.Transient
-                      (Format.sprintf "path %s is a symbolic link"
-                         (Fspath.toPrintString (Fspath.concat fspath path))))
+                      ("path " ^
+                       (Fspath.toPrintString (Fspath.concat fspath path)) ^
+                       symlinkErr))
            | _ ->
                raise (Util.Transient
                         ("path " ^
@@ -100,7 +161,7 @@ let get fromRoot fspath path =
            inode    = (* The inode number is truncated so that
                          it fits in a 31 bit ocaml integer *)
                       stats.Unix.LargeFile.st_ino land 0x3FFFFFFF;
-           desc     = Props.get stats osxInfos;
+           desc     = getProps fspath path stats osxInfos;
            osX      = osxInfos }
        with
          Unix.Unix_error((Unix.ENOENT | Unix.ENOTDIR),_,_) ->
@@ -109,6 +170,26 @@ let get fromRoot fspath path =
            desc     = Props.dummy;
            osX      = Osx.getFileInfos fspath path `ABSENT })
 
+let getType fromRoot fspath path =
+  (getAux fromRoot fspath path (fun _ _ _ _ -> Props.dummy)).typ
+
+let getBasic fromRoot fspath path =
+  getAux fromRoot fspath path (fun _ _ st _ -> Props.get' st)
+
+let getBasicWithRess fromRoot fspath path =
+  getAux fromRoot fspath path (fun _ _ st i -> Props.getWithRess st i)
+
+let get ?(archProps = Props.dummy) fromRoot fspath path =
+  let getProps fspath path stats typ =
+    Props.get ~archProps fspath path stats typ in
+  getAux fromRoot fspath path getProps
+
+let basic x =
+  { typ = x.typ;
+    inode = x.inode;
+    desc = x.desc;
+    osX = x.osX }
+
 let check fspath path props =
   Util.convertUnixErrorsToTransient
   "checking file information"
@@ -139,20 +220,45 @@ let set fspath path action newDesc =
   Props.set fspath path kind p;
   check fspath path p
 
-type stamp =
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [Fileinfo.stamp]. It must
+   always remain exactly the same as the type [Fileinfo.stamp] in version
+   2.51.5. *)
+type stamp251 =
     InodeStamp of int         (* inode number, for Unix systems *)
   | CtimeStamp of float       (* creation time, for windows systems *)
-    (* FIX [BCP, 3/07]: The Ctimestamp variant is actually bogus.
-      For file transfers, it appears that using the ctime to detect a
-      file change is completely ineffective as, when a file is deleted (or
-      renamed) and then replaced by another file, the new file inherits the
-      ctime of the old file.  It is slightly harmful performancewise, as
-      fastcheck expects ctime to be preserved by renaming.  Thus, we should
-      probably not use any stamp under Windows. *)
+
+type stamp =
+  | InodeStamp of int         (* inode number, for Unix systems *)
+  | NoStamp
+  | RescanStamp               (* stamp indicating file should be rescanned
+                                 (perhaps because previous transfer failed) *)
+
+let mstamp = Umarshal.(sum3 int unit unit
+                         (function
+                          | InodeStamp a -> I31 a
+                          | NoStamp -> I32 ()
+                          | RescanStamp -> I33 ())
+                         (function
+                          | I31 a -> InodeStamp a
+                          | I32 () -> NoStamp
+                          | I33 () -> RescanStamp))
+
+let stamp_to_compat251 (st : stamp) : stamp251 =
+  match st with
+  | InodeStamp i -> InodeStamp i
+  | NoStamp -> CtimeStamp 0.0
+  | RescanStamp -> InodeStamp (-1)
+
+let stamp_of_compat251 (st : stamp251) : stamp =
+  match st with
+  | InodeStamp i -> if i <> -1 then InodeStamp i else RescanStamp
+  | CtimeStamp _ -> NoStamp
 
 let ignoreInodeNumbers =
   Prefs.createBool "ignoreinodenumbers" false
-    "!ignore inode number changes when detecting updates"
+    ~category:(`Advanced `Syncprocess)
+    "ignore inode number changes when detecting updates"
     ("When set to true, this preference makes Unison not take advantage \
       of inode numbers during fast update detection. \
       This switch should be used with care, as it \
@@ -161,17 +267,15 @@ let ignoreInodeNumbers =
 let _ = Prefs.alias ignoreInodeNumbers "pretendwin"
 
 let stamp info =
-       (* Was "CtimeStamp info.ctime", but this is bogus: Windows
-          ctimes are not reliable. *)
-  if Prefs.read ignoreInodeNumbers then CtimeStamp 0.0 else
-  if Fs.hasInodeNumbers () then InodeStamp info.inode else CtimeStamp 0.0
+  if Prefs.read ignoreInodeNumbers then NoStamp else
+  if Fs.hasInodeNumbers () then InodeStamp info.inode else NoStamp
 
 let ressStamp info = Osx.stamp info.osX
 
 let unchanged fspath path info =
   (* The call to [Util.time] must be before the call to [get] *)
   let t0 = Util.time () in
-  let info' = get true fspath path in
+  let info' = get ~archProps:info.desc true fspath path in
   let dataUnchanged =
     Props.same_time info.desc info'.desc
       &&
@@ -186,24 +290,3 @@ let unchanged fspath path info =
   (info', dataUnchanged,
    Osx.ressUnchanged info.osX.Osx.ressInfo info'.osX.Osx.ressInfo
      (Some t0) dataUnchanged)
-
-(****)
-
-let get' f =
-  Util.convertUnixErrorsToTransient
-  "querying file information"
-    (fun () ->
-       try
-         let stats = System.stat f in
-         let typ = `FILE in
-         let osxInfos = Osx.defaultInfos typ in
-         { typ   = typ;
-           inode = stats.Unix.LargeFile.st_ino land 0x3FFFFFFF;
-           desc  = Props.get stats osxInfos;
-           osX   = osxInfos }
-       with
-         Unix.Unix_error((Unix.ENOENT | Unix.ENOTDIR),_,_) ->
-         { typ = `ABSENT;
-           inode    = 0;
-           desc     = Props.dummy;
-           osX      = Osx.defaultInfos `ABSENT })
diff --git a/src/fileinfo.mli b/src/fileinfo.mli
index 7d5242d..e84324a 100644
--- a/src/fileinfo.mli
+++ b/src/fileinfo.mli
@@ -2,22 +2,49 @@
 (* Copyright 1999-2020, Benjamin C. Pierce (see COPYING for details) *)
 
 type typ = [`ABSENT | `FILE | `DIRECTORY | `SYMLINK]
+val mtyp : typ Umarshal.t
 val type2string : typ -> string
 
-type t = { typ : typ; inode : int; desc : Props.t; osX : Osx.info}
+type t251 = { typ : typ; inode : int; desc : Props.t251; osX : Osx.info}
+type ('a, 'b) info = private { typ : typ; inode : int; desc : 'a; osX : Osx.info }
+     constraint 'a = _ Props.props
+type t = (Props.t, [`WithRess]) info
+type basic = (Props.basic, [`NoRess]) info
+type bress = (Props.basic, [`WithRess]) info
 
-val get : bool (* fromRoot *) -> Fspath.t -> Path.local -> t
+val basic : bress -> basic
+
+val m : t Umarshal.t
+val mbasic : basic Umarshal.t
+
+val to_compat251 : basic -> t251
+val of_compat251 : t251 -> basic
+
+val getType : bool (* fromRoot *) -> Fspath.t -> Path.local -> typ
+val getBasic : bool (* fromRoot *) -> Fspath.t -> Path.local -> basic
+val getBasicWithRess : bool (* fromRoot *) -> Fspath.t -> Path.local -> bress
+val get : ?archProps:Props.t -> bool (* fromRoot *) -> Fspath.t -> Path.local -> t
 val set : Fspath.t -> Path.local ->
-          [`Set of Props.t | `Copy of Path.local | `Update of Props.t] ->
-          Props.t -> unit
-val get' : System.fspath -> t
+          [`Set of Props.basic | `Copy of Path.local | `Update of Props.t] ->
+          Props.x -> unit
 
 (* IF THIS CHANGES, MAKE SURE TO INCREMENT THE ARCHIVE VERSION NUMBER!       *)
-type stamp =
+type stamp251 =
     InodeStamp of int         (* inode number, for Unix systems *)
   | CtimeStamp of float       (* creation time, for windows systems *)
 
-val stamp : t -> stamp
+type stamp =
+  | InodeStamp of int         (* inode number, for Unix systems *)
+  | NoStamp
+  | RescanStamp               (* stamp indicating file should be rescanned
+                                 (perhaps because previous transfer failed) *)
+
+val mstamp : stamp Umarshal.t
+
+val stamp_to_compat251 : stamp -> stamp251
+val stamp_of_compat251 : stamp251 -> stamp
+
+val stamp : _ info -> stamp
 
 val ressStamp : t -> Osx.ressStamp
 
@@ -28,4 +55,5 @@ val unchanged : Fspath.t -> Path.local -> t -> (t * bool * bool)
 
 val init : bool -> unit
 val allowSymlinks : [`True|`False|`Default] Prefs.t
+val shouldIgnore : string (* error message *) -> bool
 val ignoreInodeNumbers : bool Prefs.t
diff --git a/src/files.ml b/src/files.ml
index f6c0dac..28a37ff 100644
--- a/src/files.ml
+++ b/src/files.ml
@@ -46,20 +46,26 @@ let writeCommitLog source target tempname =
        Printf.fprintf c "(and delete this notice when you've done so).\n";
        close_out c)
 
-let clearCommitLog pathTo =
+let clearCommitLog tmpName =
   debug (fun() -> (Util.msg "Deleting commit log\n"));
 
-  let tmpPathDir = Fspath.canonize (Some Util.unisonDirStr) in  (* tmpPathDir is a Fspath.t *)
-  (* Use pathTo in the temporary name (instead of DANGER.README) to reduce chance of reuse *)
-  let tmpPath = Os.tempPath tmpPathDir pathTo in  (* tmpPath is a Path.local *)
-  let dangerFspath = Fspath.canonize (Some (System.fspathToString commitLogName)) in
-  let dangerFsPathTmp = Fspath.concat tmpPathDir tmpPath in
+  let commitLogNameWin () =
+    (* Work around an issue in Windows where unlink may not be immediate. *)
+    let p = commitLogName ^ (Filename.basename (Path.toString tmpName)) in
+    let rec tmp n =
+      let p = p ^ (string_of_int n) in
+      if System.file_exists p then tmp (n + 1)
+      else (System.rename commitLogName p; p)
+    in
+    try tmp 0 with
+    | Sys_error _ | Unix.Unix_error _ -> commitLogName
+  in
+  let commitLogUnlinkPath =
+    if Util.osType = `Win32 then commitLogNameWin () else commitLogName in
 
-  Os.renameFspath "DANGER.README" dangerFspath dangerFsPathTmp;
-  
   Util.convertUnixErrorsToFatal
     "clearing commit log"
-      (fun () -> System.unlink (System.fspathFromString (Fspath.toString dangerFsPathTmp)) )
+      (fun () -> System.unlink commitLogUnlinkPath)
 
 let processCommitLog () =
   if System.file_exists commitLogName then begin
@@ -68,21 +74,22 @@ let processCommitLog () =
             "Warning: the previous run of %s terminated in a dangerous state.
             Please consult the file %s, delete it, and try again."
                 Uutil.myName
-                (System.fspathToPrintString commitLogName)))
+                commitLogName))
   end else
     Lwt.return ()
 
 let processCommitLogOnHost =
-  Remote.registerHostCmd "processCommitLog" processCommitLog
+  Remote.registerHostCmd "processCommitLog" Umarshal.unit Umarshal.unit processCommitLog
 
 let processCommitLogs() =
   Lwt_unix.run
-    (Globals.allHostsIter (fun h -> processCommitLogOnHost h ()))
+    (Globals.allRootsIter (fun r -> processCommitLogOnHost r ()))
 
 (* ------------------------------------------------------------ *)
 
 let copyOnConflict = Prefs.createBool "copyonconflict" false
-  "!keep copies of conflicting files"
+  ~category:(`Advanced `Syncprocess)
+  "keep copies of conflicting files"
   "When this flag is set, Unison will make a copy of files that would \
    otherwise be overwritten or deleted in case of conflicting changes, \
    and more generally whenever the default behavior is overridden. \
@@ -92,7 +99,7 @@ let copyOnConflict = Prefs.createBool "copyonconflict" false
 
 let prepareCopy workingDir path notDefault =
   if notDefault && Prefs.read copyOnConflict then begin
-    match (Fileinfo.get true workingDir path).Fileinfo.typ with
+    match Fileinfo.getType true workingDir path with
     | `ABSENT -> Some (workingDir, path, None)
     | _ ->
       begin
@@ -143,7 +150,15 @@ let deleteLocal (fspathTo, (pathTo, ui, notDefault)) =
   Update.replaceArchiveLocal fspathTo localPathTo Update.NoArchive;
   Lwt.return ()
 
-let deleteOnRoot = Remote.registerRootCmd "delete" deleteLocal
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspathTo, (pathTo, ui, notDefault)) ->
+       (fspathTo, (pathTo, Common.ui_to_compat251 ui, notDefault)))
+  (fun (fspathTo, (pathTo, ui, notDefault)) ->
+       (fspathTo, (pathTo, Common.ui_of_compat251 ui, notDefault)))
+
+let deleteOnRoot = Remote.registerRootCmd "delete" ~convV0
+  Umarshal.(prod3 Path.m Common.mupdateItem bool id id) Umarshal.unit
+  deleteLocal
 
 let delete rootFrom pathFrom rootTo pathTo ui notDefault =
   deleteOnRoot rootTo (pathTo, ui, notDefault) >>= fun _ ->
@@ -162,18 +177,49 @@ let setPropLocal (fspath, (path, ui, newDesc, oldDesc)) =
   let localPath = Update.translatePathLocal fspath path in
   let (workingDir,realPath) = Fspath.findWorkingDir fspath localPath in
   Fileinfo.set workingDir realPath (`Update oldDesc) newDesc;
+  let newDesc = Props.purgeExtData newDesc in
   if fileUpdated ui then Stasher.stashCurrentVersion fspath localPath None;
   (* Archive update must be done last *)
   Update.updateProps fspath localPath (Some newDesc) ui;
   Lwt.return ()
 
-let setPropOnRoot = Remote.registerRootCmd "setProp" setPropLocal
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspath, (path, ui, newDesc, oldDesc)) ->
+       (fspath, (path, Common.ui_to_compat251 ui,
+         Props.to_compat251 newDesc, Props.to_compat251 oldDesc)))
+  (fun (fspath, (path, ui, newDesc, oldDesc)) ->
+       (fspath, (path, Common.ui_of_compat251 ui,
+         Props.of_compat251 newDesc, Props.of_compat251 oldDesc)))
+
+let setPropOnRoot = Remote.registerRootCmd "setProp" ~convV0
+  Umarshal.(prod4 Path.m Common.mupdateItem Props.mx Props.m id id) Umarshal.unit
+  setPropLocal
+
+let propOpt_to_compat251 = function
+  | Some prop -> Some (Props.to_compat251 prop)
+  | None -> None
+
+let propOpt_of_compat251 = function
+  | Some prop -> Some (Props.of_compat251 prop)
+  | None -> None
+
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspath, (path, propOpt, ui)) ->
+       (fspath, (Path.makeGlobal path, propOpt_to_compat251 propOpt,
+         Common.ui_to_compat251 ui)))
+  (fun (fspath, (path, propOpt, ui)) ->
+       (fspath, (Path.forceLocal path,
+         propOpt_of_compat251 propOpt, Common.ui_of_compat251 ui)))
 
 let updatePropsOnRoot =
   Remote.registerRootCmd
-   "updateProps"
+   "updateProps" ~convV0
+   Umarshal.(prod3 Path.mlocal (option Props.m) Common.mupdateItem id id)
+   Umarshal.unit
      (fun (fspath, (path, propOpt, ui)) ->
-        let localPath = Update.translatePathLocal fspath path in
+        (* Previous versions of this function received a global path as input *)
+        let localPath = if Props.xattrEnabled () then path
+          else Update.translatePathLocal fspath (Path.makeGlobal path) in
         (* Archive update must be done first *)
         Update.updateProps fspath localPath propOpt ui;
         if fileUpdated ui then
@@ -193,16 +239,26 @@ let setProp rootFrom pathFrom rootTo pathTo newDesc oldDesc uiFrom uiTo =
       (Props.toString newDesc)
       (root2string rootTo) (Path.toString pathTo)
       (Props.toString oldDesc));
+  Copy.readPropsExtDataG rootFrom pathFrom newDesc >>= fun (p, newDesc) ->
   setPropOnRoot rootTo (pathTo, uiTo, newDesc, oldDesc) >>= fun _ ->
-  updateProps rootFrom pathFrom None uiFrom
+  (match p with
+  | None -> Update.translatePath rootFrom pathFrom
+  | Some path -> Lwt.return path) >>= fun localPathFrom ->
+  updateProps rootFrom localPathFrom None uiFrom
 
 (* ------------------------------------------------------------ *)
 
+let convV0 = Remote.makeConvV0FunRet
+  (fun (b, desc) -> (b, Props.to_compat251 desc))
+  (fun (b, desc) -> (b, Props.of_compat251 desc))
+
 let mkdirOnRoot =
   Remote.registerRootCmd
-    "mkdir"
+    "mkdir" ~convV0
+    Umarshal.(prod2 Fspath.m Path.mlocal id id)
+    Umarshal.(prod2 bool Props.mbasic id id)
     (fun (fspath,(workingDir,path)) ->
-       let info = Fileinfo.get false workingDir path in
+       let info = Fileinfo.getBasic false workingDir path in
        if info.Fileinfo.typ = `DIRECTORY then begin
          begin try
            (* Make sure the directory is writable *)
@@ -213,13 +269,23 @@ let mkdirOnRoot =
        end else begin
          if info.Fileinfo.typ <> `ABSENT then
            Os.delete workingDir path;
-         Os.createDir workingDir path Props.dirDefault;
-         Lwt.return (false, (Fileinfo.get false workingDir path).Fileinfo.desc)
+         Os.createDir workingDir path (Props.perms Props.dirDefault);
+         Lwt.return (false, (Fileinfo.getBasic false workingDir path).desc)
        end)
 
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspath, (workingDir, path, initialDesc, newDesc)) ->
+       (fspath, (workingDir, path,
+         Props.to_compat251 initialDesc, Props.to_compat251 newDesc)))
+  (fun (fspath, (workingDir, path, initialDesc, newDesc)) ->
+       (fspath, (workingDir, path,
+         Props.of_compat251 initialDesc, Props.of_compat251 newDesc)))
+
 let setDirPropOnRoot =
   Remote.registerRootCmd
-    "setDirProp"
+    "setDirProp" ~convV0
+    Umarshal.(prod4 Fspath.m Path.mlocal Props.mbasic Props.mx id id)
+    Umarshal.unit
     (fun (_, (workingDir, path, initialDesc, newDesc)) ->
       Fileinfo.set workingDir path (`Set initialDesc) newDesc;
       Lwt.return ())
@@ -227,6 +293,8 @@ let setDirPropOnRoot =
 let makeSymlink =
   Remote.registerRootCmd
     "makeSymlink"
+    Umarshal.(prod3 Fspath.m Path.mlocal string id id)
+    Umarshal.unit
     (fun (fspath, (workingDir, path, l)) ->
        if Os.exists workingDir path then
          Os.delete workingDir path;
@@ -248,15 +316,15 @@ let performRename fspathTo localPathTo workingDir pathFrom pathTo prevArch =
        (Fspath.toDebugString source) (Fspath.toDebugString target))
     (fun () ->
       debugverbose (fun() ->
-        Util.msg "calling Fileinfo.get from renameLocal\n");
+        Util.msg "calling Fileinfo.getType from renameLocal\n");
       let filetypeFrom =
-        (Fileinfo.get false source Path.empty).Fileinfo.typ in
+        Fileinfo.getType false source Path.empty in
       debugverbose (fun() ->
-        Util.msg "back from Fileinfo.get from renameLocal\n");
+        Util.msg "back from Fileinfo.getType from renameLocal\n");
       if filetypeFrom = `ABSENT then raise (Util.Transient (Printf.sprintf
            "Error while renaming %s to %s -- source file has disappeared!"
            (Fspath.toPrintString source) (Fspath.toPrintString target)));
-      let filetypeTo = (Fileinfo.get false target Path.empty).Fileinfo.typ in
+      let filetypeTo = Fileinfo.getType false target Path.empty in
 
        (* Windows and Unix operate differently if the target path of a
           rename already exists: in Windows an exception is raised, in
@@ -297,7 +365,7 @@ let performRename fspathTo localPathTo workingDir pathFrom pathTo prevArch =
                        (Fspath.toDebugString target));
               Os.rename "renameLocal(2)"
                 source Path.empty target Path.empty))
-          (fun _ -> clearCommitLog pathTo);
+          (fun _ -> clearCommitLog tmpPath);
         (* It is ok to leave a temporary file.  So, the log can be
            cleared before deleting it. *)
         Os.delete temp Path.empty
@@ -327,7 +395,7 @@ let performRename fspathTo localPathTo workingDir pathFrom pathTo prevArch =
    either locally or on the other side. *)
 let renameLocal
       (fspathTo,
-       (localPathTo, workingDir, pathFrom, pathTo, ui, archOpt, notDefault)) =
+       ((localPathTo, workingDir, pathFrom, pathTo), (ui, archOpt, notDefault))) =
   let copyInfo = prepareCopy workingDir pathTo notDefault in
   (* Make sure the target is unchanged, then do the rename.
      (Note that there is an unavoidable race condition here...) *)
@@ -348,7 +416,33 @@ let renameLocal
   end;
   Lwt.return ()
 
-let renameOnHost = Remote.registerRootCmd "rename" renameLocal
+let archOpt_to_compat251 = function
+  | Some arch -> Some (Update.to_compat251 arch)
+  | None -> None
+
+let archOpt_of_compat251 = function
+  | Some arch -> Some (Update.of_compat251 arch)
+  | None -> None
+
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspathTo,
+         ((localPathTo, workingDir, pathFrom, pathTo), (ui, archOpt, notDefault))) ->
+       (fspathTo,
+         (localPathTo, workingDir, pathFrom, pathTo,
+         Common.ui_to_compat251 ui, archOpt_to_compat251 archOpt, notDefault)))
+  (fun (fspathTo,
+         (localPathTo, workingDir, pathFrom, pathTo, ui, archOpt, notDefault)) ->
+       (fspathTo,
+         ((localPathTo, workingDir, pathFrom, pathTo),
+         (Common.ui_of_compat251 ui, archOpt_of_compat251 archOpt, notDefault))))
+
+let mrename = Umarshal.(prod2
+                          (prod4 Path.mlocal Fspath.m Path.mlocal Path.mlocal id id)
+                          (prod3 Common.mupdateItem (option Update.marchive) bool id id)
+                          id id)
+
+let renameOnHost =
+  Remote.registerRootCmd "rename" ~convV0 mrename Umarshal.unit renameLocal
 
 let rename root localPath workingDir pathOld pathNew ui archOpt notDefault =
   debug (fun() ->
@@ -357,7 +451,7 @@ let rename root localPath workingDir pathOld pathNew ui archOpt notDefault =
       (Path.toString localPath)
       (Path.toString pathOld) (Path.toString pathNew));
   renameOnHost root
-    (localPath, workingDir, pathOld, pathNew, ui, archOpt, notDefault)
+    ((localPath, workingDir, pathOld, pathNew), (ui, archOpt, notDefault))
 
 (* ------------------------------------------------------------ *)
 
@@ -383,8 +477,10 @@ let setupTargetPathsLocal (fspath, path) =
   let tempPath = Os.tempPath ~fresh:false workingDir realPath in
   Lwt.return (workingDir, realPath, tempPath, localPath)
 
+let msetupTargetPaths = Umarshal.(prod4 Fspath.m Path.mlocal Path.mlocal Path.mlocal id id)
+
 let setupTargetPaths =
-  Remote.registerRootCmd "setupTargetPaths" setupTargetPathsLocal
+  Remote.registerRootCmd "setupTargetPaths" Path.m msetupTargetPaths setupTargetPathsLocal
 
 let rec createDirectories fspath localPath props =
   match props with
@@ -413,10 +509,27 @@ let setupTargetPathsAndCreateParentDirectoryLocal (fspath, (path, props)) =
   let tempPath = Os.tempPath ~fresh:false workingDir realPath in
   Lwt.return (workingDir, realPath, tempPath, localPath)
 
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspath, (path, props)) ->
+       (fspath, (path, Safelist.map Props.to_compat251 props)))
+  (fun (fspath, (path, props)) ->
+       (fspath, (path, Safelist.map Props.of_compat251 props)))
+
 let setupTargetPathsAndCreateParentDirectory =
-  Remote.registerRootCmd "setupTargetPathsAndCreateParentDirectory"
+  Remote.registerRootCmd "setupTargetPathsAndCreateParentDirectory" ~convV0
+    Umarshal.(prod2 Path.m (list Props.mx) id id)
+    Umarshal.(prod4 Fspath.m Path.mlocal Path.mlocal Path.mlocal id id)
     setupTargetPathsAndCreateParentDirectoryLocal
 
+let rec readParentsExtData rootFrom pathFrom acc = function
+  | [] -> Safelist.rev acc |> Lwt.return
+  | desc :: rem ->
+      match Path.deconstructRev pathFrom with
+      | None -> assert false
+      | Some (_, parentPath) ->
+          Copy.readPropsExtData rootFrom parentPath desc >>= fun desc' ->
+          readParentsExtData rootFrom parentPath (desc' :: acc) rem
+
 (* ------------------------------------------------------------ *)
 
 let updateSourceArchiveLocal (fspathFrom, (localPathFrom, uiFrom, errPaths)) =
@@ -435,8 +548,16 @@ let updateSourceArchiveLocal (fspathFrom, (localPathFrom, uiFrom, errPaths)) =
   Stasher.stashCurrentVersion fspathFrom localPathFrom None;
   Lwt.return ()
 
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspathFrom, (localPathFrom, uiFrom, errPaths)) ->
+       (fspathFrom, (localPathFrom, Common.ui_to_compat251 uiFrom, errPaths)))
+  (fun (fspathFrom, (localPathFrom, uiFrom, errPaths)) ->
+       (fspathFrom, (localPathFrom, Common.ui_of_compat251 uiFrom, errPaths)))
+
 let updateSourceArchive =
-  Remote.registerRootCmd "updateSourceArchive" updateSourceArchiveLocal
+  Remote.registerRootCmd "updateSourceArchive" ~convV0
+    Umarshal.(prod3 Path.mlocal Common.mupdateItem (list Path.mlocal) id id) Umarshal.unit
+    updateSourceArchiveLocal
 
 (* ------------------------------------------------------------ *)
 
@@ -472,7 +593,7 @@ let deleteSpuriousChildrenLocal (_, (fspathTo, pathTo, archChildren)) =
   Lwt.return ()
 
 let deleteSpuriousChildren =
-  Remote.registerRootCmd "deleteSpuriousChildren" deleteSpuriousChildrenLocal
+  Remote.registerRootCmd "deleteSpuriousChildren" Umarshal.(prod3 Fspath.m Path.mlocal (list Name.m) id id) Umarshal.unit deleteSpuriousChildrenLocal
 
 let rec normalizeProps propsFrom propsTo =
   match propsFrom, propsTo with
@@ -482,7 +603,7 @@ let rec normalizeProps propsFrom propsTo =
 
 (* ------------------------------------------------------------ *)
 
-let copyReg = Lwt_util.make_region 50
+let copyReg = Remote.lwtRegionWithConnCleanup 50
 
 let copy
       update
@@ -503,9 +624,13 @@ let copy
       "copy %s %s ---> %s %s \n"
       (root2string rootFrom) (Path.toString pathFrom)
       (root2string rootTo) (Path.toString pathTo));
+  (* Calculate source path *)
+  Update.translatePath rootFrom pathFrom >>= fun localPathFrom ->
   (* Calculate target paths *)
+  normalizeProps propsFrom propsTo
+  |> readParentsExtData rootFrom localPathFrom [] >>= fun parentProps ->
   setupTargetPathsAndCreateParentDirectory rootTo
-    (pathTo, normalizeProps propsFrom propsTo)
+    (pathTo, parentProps)
      >>= fun (workingDir, realPathTo, tempPathTo, localPathTo) ->
   (* When in Unicode case-insensitive mode, we want to create files
      with NFC normal-form filenames. *)
@@ -520,8 +645,6 @@ let copy
         | Some (name, parentPath) ->
             Path.child parentPath (Name.normalize name)
   in
-  (* Calculate source path *)
-  Update.translatePath rootFrom pathFrom >>= fun localPathFrom ->
   let errors = ref [] in
   (* Inner loop for recursive copy... *)
   let rec copyRec pFrom      (* Path to copy from *)
@@ -538,7 +661,7 @@ let copy
       (fun () ->
          match f with
            Update.ArchiveFile (desc, fp, stamp, ress) ->
-             Lwt_util.run_in_region copyReg 1 (fun () ->
+             Lwt_util.run_in_region !copyReg 1 (fun () ->
                Abort.check id;
                let stmp =
                  if Update.useFastChecking () then Some stamp else None in
@@ -552,14 +675,14 @@ let copy
                                       fp, Fileinfo.stamp info, ress'),
                   []))
          | Update.ArchiveSymlink l ->
-             Lwt_util.run_in_region copyReg 1 (fun () ->
+             Lwt_util.run_in_region !copyReg 1 (fun () ->
                debug (fun() -> Util.msg "Making symlink %s/%s -> %s\n"
                                  (root2string rootTo) (Path.toString pTo) l);
                Abort.check id;
                makeSymlink rootTo (workingDir, pTo, l) >>= fun () ->
                Lwt.return (f, []))
          | Update.ArchiveDir (desc, children) ->
-             Lwt_util.run_in_region copyReg 1 (fun () ->
+             Lwt_util.run_in_region !copyReg 1 (fun () ->
                debug (fun() -> Util.msg "Creating directory %s/%s\n"
                  (root2string rootTo) (Path.toString pTo));
                mkdirOnRoot rootTo (workingDir, pTo))
@@ -597,11 +720,12 @@ let copy
              else
                Lwt.return ()
              end >>= fun () ->
-             Lwt_util.run_in_region copyReg 1 (fun () ->
+             Copy.readPropsExtData rootFrom pFrom desc >>= fun desc' ->
+             Lwt_util.run_in_region !copyReg 1 (fun () ->
                (* We use the actual file permissions so as to preserve
                   inherited bits *)
                setDirPropOnRoot rootTo
-                 (workingDir, pTo, initialDesc, desc)) >>= fun () ->
+                 (workingDir, pTo, initialDesc, desc')) >>= fun () ->
              Lwt.return (Update.ArchiveDir (desc, newChildren),
                          List.flatten pathl)
          | Update.NoArchive ->
@@ -609,10 +733,8 @@ let copy
       (fun e ->
          match e with
            Util.Transient _ ->
-             if not (Abort.testException e) then begin
-               Abort.file id;
-               errors := e :: !errors
-             end;
+             if not (Abort.testException e) then Abort.file id;
+             errors := e :: !errors;
              Lwt.return (Update.NoArchive, [pFrom])
          | _ ->
              Lwt.fail e)
@@ -647,7 +769,8 @@ let (>>=) = Lwt.bind
 
 let diffCmd =
   Prefs.createString "diff" "diff -u OLDER NEWER"
-    "!set command for showing differences between files"
+    ~category:(`Advanced `General)
+    "set command for showing differences between files"
     ("This preference can be used to control the name and command-line "
      ^ "arguments of the system "
      ^ "utility used to generate displays of file differences.  The default "
@@ -813,11 +936,11 @@ let formatMergeCmd p f1 f2 backup out1 out2 outarch batchmode =
                 (Uutil.quotes (Path.toString p)) in
   cooked
 
-let copyBack fspathFrom pathFrom rootTo pathTo propsTo uiTo id =
+let copyBack fspathFrom pathFrom rootTo pathTo propsTo uiTo archTo id =
   setupTargetPaths rootTo pathTo
     >>= (fun (workingDirForCopy, realPathTo, tempPathTo, localPathTo) ->
-  let info = Fileinfo.get false fspathFrom pathFrom in
-  let fp = Os.fingerprint fspathFrom pathFrom info in
+  let info = Fileinfo.getBasicWithRess false fspathFrom pathFrom in
+  let fp = Os.fingerprint fspathFrom pathFrom info.Fileinfo.typ in
   let stamp = Osx.stamp info.Fileinfo.osX in
   let newprops = Props.setLength propsTo (Props.length info.Fileinfo.desc) in
   Copy.file
@@ -825,11 +948,13 @@ let copyBack fspathFrom pathFrom rootTo pathTo propsTo uiTo id =
     `Copy newprops fp None stamp id >>= fun info ->
   debugverbose (fun () -> Util.msg "rename from copyBack\n");
   rename rootTo localPathTo workingDirForCopy tempPathTo realPathTo
-    uiTo None false)
+    uiTo archTo false)
 
 let keeptempfilesaftermerge =
   Prefs.createBool
-    "keeptempfilesaftermerge" false "*" ""
+    "keeptempfilesaftermerge" false
+    ~category:(`Internal `Devel)
+    "*" ""
 
 let showStatus = function
   | Unix.WEXITED i -> Printf.sprintf "exited (%d)" i
@@ -936,10 +1061,10 @@ let merge root1 path1 ui1 root2 path2 ui2 id showMergeFn =
       Os.delete workingDirForMerge new1;
       Os.delete workingDirForMerge new2;
       Os.delete workingDirForMerge newarch;
-      let info1 = Fileinfo.get false workingDirForMerge working1 in
+      let info1 = Fileinfo.getType false workingDirForMerge working1 in
       (* FIX: Why split out the parts of the pair?  Why is it not abstract anyway??? *)
       let fp1 = Os.fingerprint workingDirForMerge working1 info1 in
-      let info2 = Fileinfo.get false workingDirForMerge working2 in
+      let info2 = Fileinfo.getType false workingDirForMerge working2 in
       let fp2 = Os.fingerprint workingDirForMerge working2 info2 in
       let cmd = formatMergeCmd
           path1
@@ -989,8 +1114,8 @@ let merge root1 path1 ui1 root2 path2 ui2 id showMergeFn =
           say (fun () -> Util.msg "Three outputs detected \n")
         else
           say (fun () -> Util.msg "Two outputs detected \n");
-        let info1 = Fileinfo.get false workingDirForMerge new1 in
-        let info2 = Fileinfo.get false workingDirForMerge new2 in
+        let info1 = Fileinfo.getType false workingDirForMerge new1 in
+        let info2 = Fileinfo.getType false workingDirForMerge new2 in
         let fp1' = Os.fingerprint workingDirForMerge new1 info1 in
         let fp2' = Os.fingerprint workingDirForMerge new2 info2 in
         if fp1'=fp2' then begin
@@ -1030,9 +1155,9 @@ let merge root1 path1 ui1 root2 path2 ui2 id showMergeFn =
 
         if working1_still_exists && working2_still_exists then begin
           say (fun () -> Util.msg "No output from merge cmd and both original files are still present\n");
-          let info1' = Fileinfo.get false workingDirForMerge working1 in
+          let info1' = Fileinfo.getType false workingDirForMerge working1 in
           let fp1' = Os.fingerprint workingDirForMerge working1 info1' in
-          let info2' = Fileinfo.get false workingDirForMerge working2 in
+          let info2' = Fileinfo.getType false workingDirForMerge working2 in
           let fp2' = Os.fingerprint workingDirForMerge working2 info2' in
           if fp1 = fp1' && fp2 = fp2' then
             raise (Util.Transient "Merge program didn't change either temp file");
@@ -1087,28 +1212,63 @@ let merge root1 path1 ui1 root2 path2 ui2 id showMergeFn =
 
       Lwt_unix.run
         (debug (fun () -> Util.msg "Committing results of merge\n");
-         copyBack workingDirForMerge working1 root1 path1 desc1 ui1 id >>= (fun () ->
-         copyBack workingDirForMerge working2 root2 path2 desc2 ui2 id >>= (fun () ->
+         let (desc1, desc2, archTo) =
          let arch_fspath = Fspath.concat workingDirForMerge workingarch in
          if Fs.file_exists arch_fspath then begin
            debug (fun () -> Util.msg "Updating unison archives for %s to reflect results of merge\n"
                    (Path.toString path1));
            if not (Stasher.shouldBackupCurrent path1) then
              Util.msg "Warning: 'backupcurrent' is not set for path %s\n" (Path.toString path1);
-           Stasher.stashCurrentVersion workingDirForMerge localPath1 (Some workingarch);
-           let infoarch = Fileinfo.get false workingDirForMerge workingarch in
-           let fp = Os.fingerprint arch_fspath Path.empty infoarch in
+           let infoarch = Fileinfo.getBasicWithRess false arch_fspath Path.empty in
+           let fp = Os.fingerprint arch_fspath Path.empty infoarch.typ in
            debug (fun () -> Util.msg "New fingerprint is %s\n" (Os.fullfingerprint_to_string fp));
+           let pseudoMergeDesc merge_desc =
+             (* Length and times (because the merge result's mtime is set in
+                both replicas) must come from the merge result. The remaining
+                props should be as close as possible to one of the original
+                files to reduce the possibility of props conflicts at the next
+                sync.
+
+                Current props, desc1 and desc2, can't be compared before having
+                same time and length (taken from the merge result). *)
+             let fixup_desc desc n =
+               let desc' = Props.setTime desc n in
+               Props.setLength desc' (Props.length n)
+             in
+             let desc1' = fixup_desc desc1 merge_desc
+             and desc2' = fixup_desc desc2 merge_desc in
+             let pref_desc =
+               if Props.similar desc1' desc2' then Some desc1 else
+               match ui1, ui2 with
+               | Updates (_, Previous (_, pdesc1, _, _)),
+                 Updates (_, Previous (_, pdesc2, _, _)) ->
+                   if Props.similar pdesc1 desc1 then Some desc1 else
+                   if Props.similar pdesc2 desc2 then Some desc2 else
+                   if Props.similar pdesc1 pdesc2 then Some pdesc1 else
+                   None (* Is it possible to arrive here? *)
+               | NoUpdates, (NoUpdates | Updates _) -> Some desc1
+               | Updates _, NoUpdates -> Some desc2
+               | _ -> None
+             in
+             match pref_desc with
+             | None -> None
+             | Some pref_desc -> Some (fixup_desc pref_desc merge_desc)
+           in
            let new_archive_entry =
-             Update.ArchiveFile
-               (Props.get (Fs.stat arch_fspath) infoarch.osX, fp,
-                Fileinfo.stamp (Fileinfo.get true arch_fspath Path.empty),
-                Osx.stamp infoarch.osX) in
-           Update.replaceArchive root1 path1 new_archive_entry >>= fun _ ->
-           Update.replaceArchive root2 path2 new_archive_entry >>= fun _ ->
-           Lwt.return ()
+             match pseudoMergeDesc infoarch.desc with
+             | None -> None
+             | Some new_arch_desc ->
+                 Some (Update.ArchiveFile (new_arch_desc, fp,
+                   Fileinfo.stamp infoarch, Osx.stamp infoarch.osX)) in
+           (Props.setTime desc1 infoarch.Fileinfo.desc,
+            Props.setTime desc2 infoarch.Fileinfo.desc,
+            new_archive_entry)
          end else
-           (Lwt.return ()) )))) )
+           (desc1, desc2, None)
+         in
+         copyBack workingDirForMerge working1 root1 path1 desc1 ui1 archTo id >>= (fun () ->
+         copyBack workingDirForMerge working2 root2 path2 desc2 ui2 archTo id >>= (fun () ->
+         Lwt.return () )))) )
     (fun _ ->
       Util.ignoreTransientErrors
         (fun () ->
diff --git a/src/files.mli b/src/files.mli
index 836db34..c4c65a5 100644
--- a/src/files.mli
+++ b/src/files.mli
@@ -16,7 +16,7 @@ val delete :
 
 (* Region used for the copying. Exported to be correctly set in transport.ml *)
 (* to the maximum number of threads                                          *)
-val copyReg : Lwt_util.region
+val copyReg : Lwt_util.region ref
 
 (* Copy a path in one replica to another path in a second replica.  The copy *)
 (* is performed atomically (or as close to atomically as the os will         *)
@@ -68,7 +68,7 @@ val diff :
 val processCommitLogs : unit -> unit
 
 (* List the files in a directory matching a pattern. *)
-val ls : System.fspath -> string -> string list
+val ls : string -> string -> string list
 
 val merge :
      Common.root                  (* first root *)
diff --git a/src/fingerprint.ml b/src/fingerprint.ml
index 00bbc03..9164fae 100644
--- a/src/fingerprint.ml
+++ b/src/fingerprint.ml
@@ -19,6 +19,8 @@
 (* INCREMENT "UPDATE.ARCHIVEFORMAT"                                          *)
 type t = string
 
+let m = Umarshal.string
+
 let pseudo_prefix = "LEN"
 
 let pseudo path len = pseudo_prefix ^ (Uutil.Filesize.toString len) ^ "@" ^
diff --git a/src/fingerprint.mli b/src/fingerprint.mli
index 8543fec..45bf54a 100644
--- a/src/fingerprint.mli
+++ b/src/fingerprint.mli
@@ -3,6 +3,8 @@
 
 type t
 
+val m : t Umarshal.t
+
 (* Os.safeFingerprint should usually be used rather than these functions *)
 val file : Fspath.t -> Path.local -> t
 val subfile : Fspath.t -> Int64.t -> Uutil.Filesize.t -> t
diff --git a/src/fpcache.ml b/src/fpcache.ml
index 11ea86c..6f0b6b4 100644
--- a/src/fpcache.ml
+++ b/src/fpcache.ml
@@ -34,6 +34,12 @@ let tbl = PathTbl.create 101
 type entry =
   int * string * (Props.t * Os.fullfingerprint * Fileinfo.stamp * Osx.ressStamp)
 
+let mentry = Umarshal.(prod3 int string
+                         (prod4 Props.m Os.mfullfingerprint Fileinfo.mstamp Osx.mressStamp id id)
+                         id id)
+
+let mentry_list = Umarshal.list mentry
+
 type state =
   { oc : out_channel;
     mutable count : int;
@@ -74,14 +80,14 @@ let read st ic =
   (* I/O errors are dealt with at a higher level *)
   let fp1 = Digest.input ic in
   let fp2 = Digest.input ic in
-  let headerSize = Marshal.header_size in
+  let headerSize = Umarshal.header_size in
   let header = Bytes.create headerSize in
   really_input ic header 0 headerSize;
   if fp1 <> Digest.bytes header then begin
     debug (fun () -> Util.msg "bad header checksum\n");
     raise End_of_file
   end;
-  let dataSize = Marshal.data_size header 0 in
+  let dataSize = Umarshal.data_size header 0 in
   let s = Bytes.create (headerSize + dataSize) in
   Bytes.blit header 0 s 0 headerSize;
   really_input ic s headerSize dataSize;
@@ -89,7 +95,13 @@ let read st ic =
     debug (fun () -> Util.msg "bad chunk checksum\n");
     raise End_of_file
   end;
-  let q : entry list = Marshal.from_bytes s 0 in
+  let q =
+    try Umarshal.from_bytes mentry_list s 0 with
+    | Umarshal.Error _ ->
+        debug (fun () -> Util.msg ("Umarshal error when reading from file, "
+                                ^^ "ignoring and continuing\n"));
+        []
+  in
   debug (fun () -> Util.msg "read chunk of %d files\n" (List.length q));
   List.iter (fun (l, p, i) -> PathTbl.add tbl (decompress st l p) i) q
 
@@ -102,8 +114,8 @@ let closeOut st =
 
 let write state =
   let q = Safelist.rev state.queue in
-  let s = Marshal.to_string q [Marshal.No_sharing] in
-  let fp1 = Digest.substring s 0 Marshal.header_size in
+  let s = Umarshal.to_string mentry_list q in
+  let fp1 = Digest.substring s 0 Umarshal.header_size in
   let fp2 = Digest.string s in
   begin try
     Digest.output state.oc fp1; Digest.output state.oc fp2;
@@ -127,7 +139,7 @@ let finish () =
                closeOut st
   | None    -> ()
 
-let magic = "Unison fingerprint cache format 2"
+let magic = "Unison fingerprint cache format 3"
 
 let init fastCheck ignorearchives fspath =
   finish ();
@@ -218,11 +230,10 @@ let dataClearlyUnchanged fastCheck path info desc stamp =
   match stamp with
     Fileinfo.InodeStamp inode ->
       info.Fileinfo.inode = inode
-  | Fileinfo.CtimeStamp ctime ->
-      (* BCP [Apr 07]: This doesn't work -- ctimes are unreliable
-                       under windows.  :-(
-         info.Fileinfo.ctime = ctime *)
+  | Fileinfo.NoStamp ->
       true
+  | Fileinfo.RescanStamp ->
+      false
 
 let ressClearlyUnchanged fastCheck info ress dataClearlyUnchanged =
   fastCheck
@@ -237,8 +248,9 @@ let clearlyUnchanged fastCheck path newInfo oldDesc oldStamp oldRess =
   du && ressClearlyUnchanged fastCheck newInfo oldRess du
 
 let fastercheckUNSAFE =
-  Prefs.createBool "fastercheckUNSAFE"
-    false "!skip computing fingerprints for new files (experts only!)"
+  Prefs.createBool "fastercheckUNSAFE" false
+    ~category:`Expert
+    "skip computing fingerprints for new files (experts only!)"
     (  "THIS FEATURE IS STILL EXPERIMENTAL AND SHOULD BE USED WITH EXTREME CAUTION.  "
        ^ "\n\n"
        ^ "When this flag is set to {\\tt true}, Unison will compute a 'pseudo-"
@@ -290,7 +302,7 @@ let fingerprint ?(newfile=false) fastCheck currfspath path info optFp =
         if Prefs.read fastercheckUNSAFE && newfile then begin
           debug (fun()-> Util.msg "skipping initial fingerprint of %s\n"
                             (Fspath.toDebugString (Fspath.concat currfspath path)));
-          (Fileinfo.get false currfspath path,
+          (Fileinfo.get ~archProps:info.desc false currfspath path,
            Os.pseudoFingerprint path (Props.length info.Fileinfo.desc))
         end else begin
           Os.safeFingerprint currfspath path info optFp
diff --git a/src/fpcache.mli b/src/fpcache.mli
index 50e7981..884a487 100644
--- a/src/fpcache.mli
+++ b/src/fpcache.mli
@@ -2,7 +2,7 @@
 (* Copyright 1999-2020, Benjamin C. Pierce (see COPYING for details) *)
 
 (* Initialize the cache *)
-val init : bool -> bool -> System.fspath -> unit
+val init : bool -> bool -> string -> unit
 
 (* Close the cache file and clear the in-memory cache *)
 val finish : unit -> unit
diff --git a/src/fs.ml b/src/fs.ml
index fe2fd19..8a14852 100644
--- a/src/fs.ml
+++ b/src/fs.ml
@@ -15,47 +15,50 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
-module System = System_impl.Fs
-
 type fspath = Fspath.t
+let mfspath = Fspath.m
 type dir_handle = System.dir_handle
                 = { readdir : unit -> string; closedir : unit -> unit }
 
-let symlink l f = System.symlink l (Fspath.toString f)
+let path p = Fspath.toString p |> System.extendedPath
+
+(****)
+
+let symlink l f = System.symlink l (path f)
 
-let readlink f = System.readlink (Fspath.toString f)
+let readlink f = System.readlink (path f)
 
-let chown f usr grp = System.chown (Fspath.toString f) usr grp
+let chown f usr grp = System.chown (path f) usr grp
 
-let chmod f mode = System.chmod (Fspath.toString f) mode
+let chmod f mode = System.chmod (path f) mode
 
-let utimes f t1 t2 = System.utimes (Fspath.toString f) t1 t2
+let utimes f t1 t2 = System.utimes (path f) t1 t2
 
-let unlink f = System.unlink (Fspath.toString f)
+let unlink f = System.unlink (path f)
 
-let rmdir f = System.rmdir (Fspath.toString f)
+let rmdir f = System.rmdir (path f)
 
-let mkdir f mode = System.mkdir (Fspath.toString f) mode
+let mkdir f mode = System.mkdir (path f) mode
 
-let rename f f' = System.rename (Fspath.toString f) (Fspath.toString f')
+let rename f f' = System.rename (path f) (path f')
 
-let stat f = System.stat (Fspath.toString f)
+let stat f = System.stat (path f)
 
-let lstat f = System.lstat (Fspath.toString f)
+let lstat f = System.lstat (path f)
 
-let openfile f flags perms = System.openfile (Fspath.toString f) flags perms
+let openfile f flags perms = System.openfile (path f) flags perms
 
-let opendir f = System.opendir (Fspath.toString f)
+let opendir f = System.opendir (path f)
 
 let open_in_gen flags mode f =
-  System.open_in_gen flags mode (Fspath.toString f)
+  System.open_in_gen flags mode (path f)
 
 let open_out_gen flags mode f =
-  System.open_out_gen flags mode (Fspath.toString f)
+  System.open_out_gen flags mode (path f)
 
 (****)
 
-let open_in_bin f = open_in_gen [Open_rdonly; Open_binary] 0 f
+let open_in_bin f = System.open_in_bin (path f)
 
 let file_exists f =
   try
@@ -65,11 +68,24 @@ let file_exists f =
 
 (****)
 
-let fingerprint f = System.fingerprint (Fspath.toString f)
+exception XattrNotSupported = System.XattrNotSupported
+
+let xattr_list f = System.xattr_list (path f)
+let xattr_get f n = System.xattr_get (path f) n
+let xattr_set f n v = System.xattr_set (path f) n v
+let xattr_remove f n = System.xattr_remove (path f) n
+
+let xattrUpdatesCTime = System.xattrUpdatesCTime
+
+(****)
+
+let acl_get_text f = System.acl_get_text (path f)
+let acl_set_text f acl = System.acl_set_text (path f) acl
+
+(****)
+
+let fingerprint f = System.fingerprint (path f)
 
-let canSetTime f = System.canSetTime (Fspath.toString f)
 let hasInodeNumbers () = System.hasInodeNumbers ()
 let hasSymlink () = System.hasSymlink ()
 let hasCorrectCTime = System.hasCorrectCTime
-
-let setUnicodeEncoding = System.setUnicodeEncoding
diff --git a/src/fs.mli b/src/fs.mli
index f77da51..2aedc54 100644
--- a/src/fs.mli
+++ b/src/fs.mli
@@ -4,5 +4,3 @@
 (* Operations on fspaths *)
 
 include System_intf.Core with type fspath = Fspath.t
-
-val setUnicodeEncoding : bool -> unit
diff --git a/src/fsmonitor.py b/src/fsmonitor.py
index 2def7dd..03ee2be 100644
--- a/src/fsmonitor.py
+++ b/src/fsmonitor.py
@@ -323,7 +323,7 @@ if sys.platform == 'darwin':
             #make a list of all files in question (all files in path w/o dirs)
             try:
                     names = os.listdir(path)
-            except os.error, msg:
+            except os.error:
             #path does not exist (anymore?). Add it to the results
                     mydebug("adding nonexisting path %s for sync",path)
                     result.append(path)
@@ -541,7 +541,7 @@ if sys.platform == 'win32':
                         while 1:
                                 sleep(3600)
                 except KeyboardInterrupt:
-                        print "Cleaning up."
+                        print("Cleaning up.")
 
 #################################################
 # END Windows specific code
@@ -664,7 +664,7 @@ to read all the settings from there."""
             while sys.stdin.readline(): pass
             os._exit(0)
         t = threading.Thread(target=exitThread)
-        t.setDaemon(True)
+        t.daemon = True
         t.start()
 
         if sys.platform=='darwin':
diff --git a/src/fsmonitor/linux/Makefile b/src/fsmonitor/linux/Makefile
index f6905d8..223d82e 100644
--- a/src/fsmonitor/linux/Makefile
+++ b/src/fsmonitor/linux/Makefile
@@ -20,9 +20,9 @@ endif
 
 buildexecutable:: $(FSMONITOR)$(EXEC_EXT)
 
-$(FSMONITOR)$(EXEC_EXT): $(COMPATOCAMLOBJS) $(FSMCAMLOBJS) $(FSMCOBJS)
+$(FSMONITOR)$(EXEC_EXT): $(FSMCAMLOBJS) $(FSMCOBJS)
 	@echo Linking $@
-	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
+	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CAMLCFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
 
 clean::
 	rm -f $(DIR)/*.cm[iox] $(DIR)/*.o $(DIR)/*~
diff --git a/src/fsmonitor/linux/dune b/src/fsmonitor/linux/dune
index 5c555c8..c9c6a81 100644
--- a/src/fsmonitor/linux/dune
+++ b/src/fsmonitor/linux/dune
@@ -1,11 +1,17 @@
 (copy_files# ../watchercommon.ml{,i})
 
 (library
- (name fswatcher)
+ (name fswatcher_linux)
  (wrapped false)
- (enabled_if (= %{system} "linux"))
+ (enabled_if
+  (or
+   (= %{system} "linux")
+   (= %{system} "linux_elf")
+   (= %{system} "elf")
+   (= %{system} "linux_eabihf")
+   (= %{system} "linux_eabi")))
  (modules :standard \ watcher)
- (flags :standard -w -3-27-39)
+ (flags :standard -w -3-27)
  (foreign_stubs
   (language c)
   (names inotify_stubs))
@@ -14,7 +20,14 @@
 (executable
  (name watcher)
  (public_name unison-fsmonitor)
- (enabled_if (= %{system} "linux"))
+ (package unison-fsmonitor)
+ (enabled_if
+  (or
+   (= %{system} "linux")
+   (= %{system} "linux_elf")
+   (= %{system} "elf")
+   (= %{system} "linux_eabihf")
+   (= %{system} "linux_eabi")))
  (modules watcher)
  (flags :standard -w -27)
- (libraries fswatcher))
+ (libraries fswatcher_linux))
diff --git a/src/fsmonitor/linux/inotify.ml b/src/fsmonitor/linux/inotify.ml
index d4371a7..5a19569 100644
--- a/src/fsmonitor/linux/inotify.ml
+++ b/src/fsmonitor/linux/inotify.ml
@@ -14,8 +14,6 @@
  * Inotify OCaml binding
  *)
 
-exception Error of string * int
-
 type select_event =
         | S_Access
         | S_Attrib
@@ -115,5 +113,3 @@ let read fd =
         done;
 
         List.rev !ret
-
-let _ = Callback.register_exception "inotify.error" (Error ("register_callback", 0))
diff --git a/src/fsmonitor/linux/inotify.mli b/src/fsmonitor/linux/inotify.mli
index b248c9d..da79712 100644
--- a/src/fsmonitor/linux/inotify.mli
+++ b/src/fsmonitor/linux/inotify.mli
@@ -13,8 +13,6 @@
  *
  * Inotify OCaml binding
  *)
-exception Error of string * int
-
 type select_event =
 | S_Access
 | S_Attrib
diff --git a/src/fsmonitor/linux/inotify_stubs.c b/src/fsmonitor/linux/inotify_stubs.c
index 1df1f46..2715793 100644
--- a/src/fsmonitor/linux/inotify_stubs.c
+++ b/src/fsmonitor/linux/inotify_stubs.c
@@ -23,10 +23,13 @@
 #include <caml/mlvalues.h>
 #include <caml/memory.h>
 #include <caml/alloc.h>
-#include <caml/custom.h>
-#include <caml/fail.h>
-#include <caml/signals.h>
-#include <caml/callback.h>
+#include <caml/unixsupport.h>
+#include <caml/version.h>
+
+#if OCAML_VERSION_MAJOR < 5
+#define caml_unix_error unix_error
+#define caml_uerror uerror
+#endif
 
 #ifndef IN_EXCL_UNLINK
 #define IN_EXCL_UNLINK 0  /* If not supported, just ignore */
@@ -47,41 +50,30 @@ static int inotify_return_table[] = {
         IN_IGNORED, IN_ISDIR, IN_Q_OVERFLOW, IN_UNMOUNT, 0
 };
 
-static void raise_inotify_error(char const *msg)
-{
-        static const value *inotify_err = NULL;
-        value args[2];
-
-        if (!inotify_err)
-                inotify_err = caml_named_value("inotify.error");
-        args[0] = caml_copy_string(msg);
-        args[1] = Val_int(errno);
-
-        caml_raise_with_args(*inotify_err, 2, args);
-}
-
-value stub_inotify_init(value unit)
+CAMLprim value stub_inotify_init(value unit)
 {
         CAMLparam1(unit);
         int fd;
 
         fd = inotify_init();
+        if (fd == -1)
+                caml_uerror("inotify_init", Nothing);
         CAMLreturn(Val_int(fd));
 }
 
-value stub_inotify_ioctl_fionread(value fd)
+CAMLprim value stub_inotify_ioctl_fionread(value fd)
 {
         CAMLparam1(fd);
         int rc, bytes;
 
         rc = ioctl(Int_val(fd), FIONREAD, &bytes);
         if (rc == -1)
-                raise_inotify_error("ioctl fionread");
+                caml_uerror("ioctl fionread", Nothing);
 
         CAMLreturn(Val_int(bytes));
 }
 
-value stub_inotify_add_watch(value fd, value path, value mask)
+CAMLprim value stub_inotify_add_watch(value fd, value path, value mask)
 {
         CAMLparam3(fd, path, mask);
         int cv_mask, wd;
@@ -89,28 +81,28 @@ value stub_inotify_add_watch(value fd, value path, value mask)
         cv_mask = caml_convert_flag_list(mask, inotify_flag_table);
         wd = inotify_add_watch(Int_val(fd), String_val(path), cv_mask);
         if (wd < 0)
-                raise_inotify_error("add_watch");
+                caml_uerror("inotify_add_watch", Nothing);
         CAMLreturn(Val_int(wd));
 }
 
-value stub_inotify_rm_watch(value fd, value wd)
+CAMLprim value stub_inotify_rm_watch(value fd, value wd)
 {
         CAMLparam2(fd, wd);
         int ret;
 
         ret = inotify_rm_watch(Int_val(fd), Int_val(wd));
         if (ret == -1)
-                raise_inotify_error("rm_watch");
+                caml_uerror("inotify_rm_watch", Nothing);
         CAMLreturn(Val_unit);
 }
 
-value stub_inotify_struct_size(void)
+CAMLprim value stub_inotify_struct_size(void)
 {
         CAMLparam0();
         CAMLreturn(Val_int(sizeof(struct inotify_event)));
 }
 
-value stub_inotify_convert(value buf)
+CAMLprim value stub_inotify_convert(value buf)
 {
         CAMLparam1(buf);
         CAMLlocal3(event, l, tmpl);
diff --git a/src/fsmonitor/linux/watcher.ml b/src/fsmonitor/linux/watcher.ml
index f36de3e..40a2c50 100644
--- a/src/fsmonitor/linux/watcher.ml
+++ b/src/fsmonitor/linux/watcher.ml
@@ -185,7 +185,7 @@ let release_watch file =
         begin try
           Lwt_inotify.rm_watch st id
           (* Will fail with EINVAL if the file has been deleted... *)
-        with Inotify.Error (_, no) ->
+        with Unix.Unix_error _ ->
           ()
         end;
         Hashtbl.remove watcher_by_id id
@@ -214,22 +214,23 @@ let add_watch path file follow =
         Hashtbl.replace watcher_by_id id (IntSet.add (get_id file) s);
         set_watch file (Some id)
     end
-  with Inotify.Error (_, no) ->
+  with Unix.Unix_error (errno, _, _) ->
     release_watch file;
-    match no with
-      2 (* ENOENT *) ->
+    match errno with
+    | ENOENT ->
         raise Watchercommon.Already_lost
-    | 28 (* ENOSPC *) ->
+    | ENOSPC ->
         Watchercommon.error ("cannot add a watcher: system limit reached"
             ^ " (you can do a web search for \"inotify max_user_watches\""
             ^ " to understand the reasons and mitigations for this error)")
-    | 13 (* EACCES *) | 20 (* ENOTDIR *) | 40 (* ELOOP *) ->
+    | EACCES | ENOTDIR | ELOOP ->
         (* These errors should be well handled by Unison (they will
            result in errors during update detection *)
         ()
     | _ ->
         Watchercommon.error
-          (Format.sprintf "unexpected error %d while adding a watcher" no)
+          (Format.sprintf "unexpected error while adding a watcher: %s"
+          (Unix.error_message errno))
 
 end
 
diff --git a/src/fsmonitor/solaris/Makefile b/src/fsmonitor/solaris/Makefile
index d488e58..ce66420 100644
--- a/src/fsmonitor/solaris/Makefile
+++ b/src/fsmonitor/solaris/Makefile
@@ -19,9 +19,9 @@ endif
 
 buildexecutable:: $(FSMONITOR)$(EXEC_EXT)
 
-$(FSMONITOR)$(EXEC_EXT): $(COMPATOCAMLOBJS) $(FSMCAMLOBJS) $(FSMCOBJS)
+$(FSMONITOR)$(EXEC_EXT): $(FSMCAMLOBJS) $(FSMCOBJS)
 	@echo Linking $@
-	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
+	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CAMLCFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
 
 clean::
 	rm -f $(DIR)/*.cm[iox] $(DIR)/*.o $(DIR)/*~
diff --git a/src/fsmonitor/solaris/dune b/src/fsmonitor/solaris/dune
new file mode 100644
index 0000000..1f5abf7
--- /dev/null
+++ b/src/fsmonitor/solaris/dune
@@ -0,0 +1,22 @@
+(copy_files# ../watchercommon.ml{,i})
+
+(library
+ (name fswatcher_solaris)
+ (wrapped false)
+ (enabled_if (= %{system} "solaris"))
+ (modules :standard \ watcher)
+ (flags :standard -w -3-27-39)
+ (foreign_stubs
+  (language c)
+  (names fen_stubs))
+ (libraries unix lwt_lib))
+
+(executable
+ (name watcher)
+ (public_name unison-fsmonitor)
+ (package unison-fsmonitor)
+ (enabled_if (= %{system} "solaris"))
+ (modules watcher)
+ (flags :standard -w -27)
+ (libraries fswatcher_solaris))
+
diff --git a/src/fsmonitor/solaris/fen_stubs.c b/src/fsmonitor/solaris/fen_stubs.c
index acac988..b57e924 100644
--- a/src/fsmonitor/solaris/fen_stubs.c
+++ b/src/fsmonitor/solaris/fen_stubs.c
@@ -25,7 +25,18 @@
 #include <caml/memory.h>
 #include <caml/alloc.h>
 #include <caml/unixsupport.h>
+#include <caml/version.h>
 
+#if OCAML_VERSION_MAJOR < 5
+#define caml_unix_error unix_error
+#define caml_uerror uerror
+#endif
+
+
+/* FILE_TRUNC was added in illumos and may not be present in Solaris */
+#ifndef FILE_TRUNC
+#define FILE_TRUNC 0
+#endif
 
 /* We define the flags here rather than pass in from OCaml code
  * because they're constant and it reduces extra processing. */
@@ -33,13 +44,13 @@
 #define EV_FLAGS_NOFOLLOW EV_FLAGS_FOLLOW | FILE_NOFOLLOW
 
 
-CAMLprim value unsn_port_create()
+CAMLprim value unsn_port_create(value unit)
 {
   CAMLparam0();
 
   int port = port_create();
   if (port == -1) {
-    uerror("port_create", Nothing);
+    caml_uerror("port_create", Nothing);
   }
 
   CAMLreturn(Val_int(port));
@@ -51,7 +62,7 @@ CAMLprim value unsn_port_close(value v)
 
   int status = close(Int_val(v));
   if (status == -1) {
-    uerror("port_close", Nothing);
+    caml_uerror("port_close", Nothing);
   }
 
   CAMLreturn(Val_unit);
@@ -132,7 +143,7 @@ CAMLprim value unsn_port_associate(value port, value path, value follow, value c
 
   struct event_obj *eo = malloc(sizeof(struct event_obj));
   if (eo == NULL) {
-    unix_error(ENOMEM, "port_associate", path);
+    caml_unix_error(ENOMEM, "port_associate", path);
   }
 
   eo->cookie = Int_val(cookie);
@@ -141,7 +152,7 @@ CAMLprim value unsn_port_associate(value port, value path, value follow, value c
   int status = port_associate_aux(Int_val(port), eo, Bool_val(follow));
   if (status == -1) {
     free_eo(eo);
-    uerror("port_associate", path);
+    caml_uerror("port_associate", path);
   }
 
   /* Returning a malloc'ed pointer as a value is not fully safe as it's not
@@ -163,7 +174,7 @@ CAMLprim value unsn_port_reassociate(value port, value eo_val, value follow)
 
   struct event_obj *eo = EvObj_val(eo_val);
   if (eo == NULL) {
-    unix_error(EINVAL, "port_reassociate",
+    caml_unix_error(EINVAL, "port_reassociate",
         caml_copy_string("NULL eo; this indicates a BUG!"));
   }
 
@@ -193,13 +204,13 @@ CAMLprim value unsn_port_dissociate(value port, value eo_val)
 
   struct event_obj *eo = EvObj_val(eo_val);
   if (eo == NULL) {
-    unix_error(EINVAL, "port_dissociate",
+    caml_unix_error(EINVAL, "port_dissociate",
         caml_copy_string("NULL eo; this indicates a BUG!"));
   }
 
   int status = port_dissociate(Int_val(port), PORT_SOURCE_FILE, (uintptr_t) &(eo->fo));
   if (status == -1 && errno != ENOENT) {
-    uerror("port_dissociate", caml_copy_string(eo->fo.fo_name));
+    caml_uerror("port_dissociate", caml_copy_string(eo->fo.fo_name));
   }
 
   CAMLreturn(Val_unit);
@@ -229,13 +240,13 @@ CAMLprim value unsn_port_get(value port)
 
   pel = malloc(cnt * sizeof(port_event_t));
   if (pel == NULL) {
-    unix_error(ENOMEM, "port_getn", Nothing);
+    caml_unix_error(ENOMEM, "port_getn", Nothing);
   }
 
   status = port_getn(Int_val(port), pel, cnt, &cnt, &timeout);
   if (status == -1 && errno != ETIME && errno != EINTR) {
     free(pel);
-    uerror("port_getn", Nothing);
+    caml_uerror("port_getn", Nothing);
   }
 
   for (int j = 0; j < cnt; j++) {
@@ -247,7 +258,7 @@ CAMLprim value unsn_port_get(value port)
 
     if (eo == NULL) {
       free(pel);
-      unix_error(EINVAL, "portev_user",
+      caml_unix_error(EINVAL, "portev_user",
           caml_copy_string("NULL eo; this indicates a BUG!"));
     }
 
diff --git a/src/fsmonitor/watchercommon.ml b/src/fsmonitor/watchercommon.ml
index dc08bc9..732a1f9 100644
--- a/src/fsmonitor/watchercommon.ml
+++ b/src/fsmonitor/watchercommon.ml
@@ -285,7 +285,7 @@ let remove_change dir nm =
 let clear_change_table hash =
   changes := StringMap.remove hash !changes
 
-let rec clear_changes hash time =
+let clear_changes hash time =
   let rec clear_rec f =
     f.changed_children <-
       StringMap.filter
diff --git a/src/fsmonitor/windows/Makefile b/src/fsmonitor/windows/Makefile
index 64c56c1..95d28b3 100644
--- a/src/fsmonitor/windows/Makefile
+++ b/src/fsmonitor/windows/Makefile
@@ -3,6 +3,7 @@ FSMONITOR = unison-fsmonitor
 
 DIR=fsmonitor/windows
 FSMOCAMLOBJS = \
+   ubase/umarshal.cmo \
    ubase/rx.cmo unicode_tables.cmo unicode.cmo \
    system/system_generic.cmo system/system_win.cmo \
    system/win/system_impl.cmo \
@@ -10,8 +11,10 @@ FSMOCAMLOBJS = \
    lwt/win/lwt_win.cmo \
    fsmonitor/watchercommon.cmo $(DIR)/watcher.cmo
 FSMCOBJS = \
-   system/system_win_stubs.o lwt/lwt_unix_stubs.o
-FSMOCAMLLIBS=bigarray.cma unix.cma
+   bytearray_stubs$(OBJ_EXT) \
+   system/system_win_stubs$(OBJ_EXT) lwt/lwt_unix_stubs$(OBJ_EXT) \
+   props_xattr$(OBJ_EXT) props_acl$(OBJ_EXT)
+FSMOCAMLLIBS=unix.cma
 
 ifeq ($(NATIVE), true)
   FSMCAMLOBJS=$(subst .cmo,.cmx, $(FSMOCAMLOBJS))
@@ -23,10 +26,10 @@ endif
 
 buildexecutable:: $(FSMONITOR)$(EXEC_EXT)
 
-$(FSMONITOR)$(EXEC_EXT): $(COMPATOCAMLOBJS) $(FSMCAMLOBJS) $(FSMCOBJS)
+$(FSMONITOR)$(EXEC_EXT): $(FSMCAMLOBJS) $(FSMCOBJS)
 	@echo Linking $@
-	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
+	$(CAMLC) -verbose $(CAMLFLAGS) $(CAMLLDFLAGS) -o $@ $(CAMLCFLAGS) $(FSMCAMLLIBS) $^ $(CLIBS)
 
 clean::
-	rm -f $(DIR)/*.cm[iox] $(DIR)/*.o $(DIR)/*~
+	rm -f $(DIR)/*.cm[iox] $(DIR)/*.{o,obj} $(DIR)/*~
 	rm -f $(FSMONITOR)$(EXEC_EXT)
diff --git a/src/fsmonitor/windows/watcher.ml b/src/fsmonitor/windows/watcher.ml
index 5416534..928eb0b 100644
--- a/src/fsmonitor/windows/watcher.ml
+++ b/src/fsmonitor/windows/watcher.ml
@@ -80,7 +80,7 @@ let rec follow_win_path_parent root dir path pos =
 
 let get_win_path root dir ((ev_path, act) as ev) =
   (* Blindly expand the event path to long names form. If event path
-     is not found among the watched patchs then try to find the nearest
+     is not found among the watched paths then try to find the nearest
      parent directory and report a modification on it. MSDN states the
      following: "If there is both a short and long name for the file,
      [Lwt_win.readdirectorychanges] will return one of these names,
@@ -88,7 +88,7 @@ let get_win_path root dir ((ev_path, act) as ev) =
   let p = if event_kind ev = `DEL then None else
     follow_win_path dir (Lwt_win.longpathname root ev_path) 0 in
   match p with
-  | Some _ as pathnm -> (pathnm, ev)
+  | Some _ -> (p, ev)
   | None ->
     (* If path is not found or event is a deletion then look up the
        parent directory and report a modification on it. It is not
@@ -110,21 +110,11 @@ let flags =
 let watch_root_directory path dir =
   let h = Lwt_win.open_directory path in
   let path = Lwt_win.longpathname "" path in
-  let path =
-    if String.sub path 0 4 = "\\\\?\\" then begin
-      let n = String.sub path 4 (String.length path - 4) in
-      if String.sub n 0 3 = "UNC" then
-        "\\" ^ String.sub n 3 (String.length n - 3)
-      else
-        n
-    end else
-      path
-  in
   let rec loop () =
     Lwt_win.readdirectorychanges h true flags >>= fun l ->
     let time = Unix.gettimeofday () in
     List.iter
-      (fun ((ev_path, _) as ev) ->
+      (fun ev ->
          if !previous_event <> Some ev then begin
            previous_event := Some ev;
            if !Watchercommon.debug then print_event ev;
diff --git a/src/fspath.ml b/src/fspath.ml
index fde5979..cd81268 100644
--- a/src/fspath.ml
+++ b/src/fspath.ml
@@ -28,28 +28,58 @@
 (*      All fspaths are absolute                                             *)
 (*                                                                         - *)
 
-module Fs = System_impl.Fs
-
 let debug = Util.debug "fspath"
 let debugverbose = Util.debug "fsspath+"
 
 type t = Fspath of string
 
+let m = Umarshal.(sum1 string (function Fspath a -> a) (function a -> Fspath a))
+
 let toString (Fspath f) = f
 let toPrintString (Fspath f) = f
 let toDebugString (Fspath f) = String.escaped f
-let toSysPath (Fspath f) = System.fspathFromString f
 
 (* Needed to hack around some ocaml/Windows bugs, see comment at stat, below *)
-let winRootRx = Rx.rx "(([a-zA-Z]:)?/|//[^/]+/[^/]+/)"
+let winRootRx = Rx.rx "(([a-zA-Z]:)?/|//[^?/]+/[^/]+/|//[?]/[Uu][Nn][Cc]/[^/]+/[^/]+/)|//[?]/([^Uu][^/]*|[Uu]|[Uu][^Nn][^/]*|[Uu][Nn]|[Uu][Nn][^Cc][^/]*|[Uu][Nn][Cc][^/]+)/"
 (* FIX I think we could just check the last character of [d]. *)
 let isRootDir d =
 (* We assume all path separators are slashes in d                            *)
   d="/" ||
   (Util.osType = `Win32 && Rx.match_string winRootRx d)
-let winRootFixRx = Rx.rx "//[^/]+/[^/]+"
+(* Here, backslashes are allowed as path separators in Windows               *)
+let isRootDirLocalString d =
+  let d =
+    if Util.osType = `Win32 then Fileutil.backslashes2forwardslashes d else d
+  in
+  isRootDir ((Fileutil.removeTrailingSlashes d) ^ "/")
 let winRootFix d =
-  if Rx.match_string winRootFixRx d then d^"/" else d
+  if Rx.match_string winRootRx (d ^ "/") then d ^ "/" else d
+let winFNsPrefixRx = Rx.rx "[\\/][\\/][?][\\/][^\\/]+"
+let isInvalidWinPath p =
+  Rx.match_string winFNsPrefixRx p (* Is there a path after the prefix? *)
+let winSafeDirname p =
+  if Util.osType <> `Win32 then
+    Filename.dirname p
+  else
+    (* [Filename.dirname] can't handle Windows paths prefixed with \\?\
+       (Win32 file namespace) if [dirname] goes all the way up to the fs root.
+       Most paths are still processed correctly because they are basically a
+       DOS path prefixed with \\?\ or something similar to \\server\share\
+       paths. Only paths right at the fs root are problematic.
+
+       \\?\C:\ becomes \\? (correct is \\?\C:\)
+       \\?\C:\sub becomes \\?\C (correct is \\?\C:\)
+       \\?\Volume{GUID}\ becomes \\? (correct is \\?\Volume{GUID}\)
+       \\?\Volume{GUID}\sub becomes \\?\Volume{GUID} (correct is \\?\Volume{GUID}\)
+
+       As a workaround, first remove the \\?\ prefix and the first component of
+       the path (usually this would be the "volume", except for UNC paths).
+       Then add the removed prefix back to the result of [dirname]. *)
+    match Rx.match_prefix winFNsPrefixRx p 0 with
+    | None -> Filename.dirname p
+    | Some pos ->
+        String.sub p 0 pos ^
+          Filename.dirname (String.sub p pos (String.length p - pos))
 
 (* [differentSuffix: fspath -> fspath -> (string * string)] returns the      *)
 (* least distinguishing suffixes of two fspaths, for displaying in the user  *)
@@ -232,12 +262,12 @@ let canonizeFspath p0 =
   let p = match p0 with None -> "." | Some "" -> "." | Some s -> s in
   let p' =
     begin
-      let original = Fs.getcwd() in
+      let original = System.getcwd () in
       try
         let newp =
-          (Fs.chdir p; (* This might raise Sys_error *)
-           Fs.getcwd()) in
-        Fs.chdir original;
+          System.chdir p; (* This might raise Sys_error *)
+          System.getcwd () in
+        System.chdir original;
         newp
       with
         Sys_error why ->
@@ -251,18 +281,19 @@ let canonizeFspath p0 =
           (* fails, we just quit.  This works nicely for most cases of (1),  *)
           (* it works for (2), and on (3) it may leave a mess for someone    *)
           (* else to pick up.                                                *)
-          let p = if Util.osType = `Win32 then Fileutil.backslashes2forwardslashes p else p in
-          if isRootDir p then raise
+          if isRootDirLocalString p || isInvalidWinPath p then raise
             (Util.Fatal (Printf.sprintf
-               "Cannot find canonical name of root directory %s\n(%s)" p why));
-          let parent = Filename.dirname p in
+               "Cannot find canonical name of root directory %s\n(%s)%s" p why
+               (if isInvalidWinPath p then "\nMaybe you need to add a "
+                 ^ "backslash at end of the root path?" else "")));
+          let parent = winSafeDirname p in
           let parent' = begin
-            (try Fs.chdir parent with
+            (try System.chdir parent with
                Sys_error why2 -> raise (Util.Fatal (Printf.sprintf
-                 "Cannot find canonical name of %s: unable to cd either to it\
+                 "Cannot find canonical name of %s: unable to cd either to it \
 (%s)\nor to its parent %s\n(%s)" p why parent why2)));
-            Fs.getcwd() end in
-          Fs.chdir original;
+            System.getcwd () end in
+          System.chdir original;
           let bn = Filename.basename p in
           if bn="" then parent'
           else toString(child (localString2fspath parent')
@@ -302,28 +333,62 @@ let findWorkingDir fspath path =
           (Util.Transient (Printf.sprintf
              "Too many symbolic links from %s" abspath));
       try
-        let link = Fs.readlink p in
+        (* Relevant on Windows: We can (and should) use [extendedPath] only
+           on the very first input, which is known to satisfy [Fspath.t]
+           invariants. Inputs used for all following loops come from the output
+           of [readlink] either without any processing done on it (if the link
+           is an absolute path) - such paths are potentially unsuitable as
+           input to [extendedPath] - or already extended (when concatenating
+           a relative path). *)
+        let link = System.readlink (if n = 0 then System.extendedPath p else p) in
         let linkabs =
           if Filename.is_relative link then
-            Fs.fspathConcat (Fs.fspathDirname p) link
+            (* FIXME? On Windows, this concatenation will potentially create
+               an invalid path if [link] contains components like "." and "..".
+               These components will not be processed by Windows if [p] has
+               prefix \\?\ or //?/ or if the resulting path is later used as
+               input to a syscall via [Fs] module (then the said prefix could be
+               added automatically).
+               https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
+
+               The solution is perhaps to replace the entire [followlinks]
+               function with realpath(3) on POSIX platforms. The respective
+               function in Windows seems to be GetFinalPathNameByHandle, which
+               is available since Windows Vista.
+               [Unix.realpath] first appeared in OCaml 4.13.
+
+               However, realpath(3) does not have exactly the same semantics as
+               the current [followlinks] function. [followlinks] will go as far
+               as it can and gives the last successful intermediary path as the
+               result when an error happens. realpath(3) will give you all or
+               nothing.
+
+               [chdir] hack from [canonizeFspath] above seems to be the current
+               best compromise. *)
+            Filename.concat (winSafeDirname p) link
+            |> fun l ->
+              if Util.osType = `Win32 then
+                let Fspath l' = canonizeFspath (Some l) in
+                System.extendedPath l'
+              else l
           else link in
         followlinks (n+1) linkabs
       with
-        Unix.Unix_error _ -> p in
+      | Unix.Unix_error _ | Util.Fatal _ -> p
+    in
     followlinks 0 abspath in
-  if isRootDir realpath then
+  if isRootDirLocalString realpath then
     raise (Util.Transient(Printf.sprintf
                             "The path %s is a root directory" abspath));
-  let realpath = Fileutil.removeTrailingSlashes realpath in
   let p = Filename.basename realpath in
   debug
     (fun() ->
       Util.msg "Os.findWorkingDir(%s,%s) = (%s,%s)\n"
         (toString fspath)
         (Path.toString path)
-        (Filename.dirname realpath)
+        (winSafeDirname realpath)
         p);
-  (localString2fspath (Filename.dirname realpath), Path.fromString p)
+  (localString2fspath (winSafeDirname realpath), Path.fromString p)
 
 let quotes (Fspath f) = Uutil.quotes f
 let compare (Fspath f1) (Fspath f2) = compare f1 f2
diff --git a/src/fspath.mli b/src/fspath.mli
index 73f760d..74eba10 100644
--- a/src/fspath.mli
+++ b/src/fspath.mli
@@ -5,6 +5,8 @@
 
 type t
 
+val m : t Umarshal.t
+
 val child : t -> Name.t -> t
 val concat : t -> Path.local -> t
 
@@ -12,7 +14,6 @@ val canonize : string option -> t
 val toString : t -> string
 val toPrintString : t -> string
 val toDebugString : t -> string
-val toSysPath : t -> System.fspath
 
 (* If fspath+path refers to a (followed) symlink, then return the directory  *)
 (* of the symlink's target; otherwise return the parent dir of path.  If     *)
diff --git a/src/fswatch.ml b/src/fswatch.ml
index 3bcca5e..0841244 100644
--- a/src/fswatch.ml
+++ b/src/fswatch.ml
@@ -64,7 +64,7 @@ Protocol description
   only further changes.
 
   Unison can wait for changes in a replica by emitting a 'WAIT hash'
-  command. It can watch several replicas by sending a serie of these
+  command. It can watch several replicas by sending a series of these
   commands. The child process is expected to respond once, by a
   'CHANGE hash1 ... hash2' response that lists the changed replicas
   among those included in a 'WAIT' command, when changes are
@@ -172,7 +172,8 @@ end
 
 let useWatcher =
   Prefs.createBool "watch" false
-    "!when set, use a file watcher process to detect changes"
+    ~category:(`Advanced `General)
+    "when set, use a file watcher process to detect changes"
     "Unison uses a file watcher process, when available, to detect filesystem \
      changes; this is used to speed up update detection. Setting this flag to \
      false disables the use of this process."
@@ -221,26 +222,24 @@ let read_line i =
 (****)
 
 let path =
-  List.map System.fspathFromString
-    (try
+    try
        Str.split (Str.regexp (if Util.osType = `Win32 then ";" else ":"))
          (Sys.getenv "PATH")
      with Not_found ->
-       [])
+       []
 
 let search_in_path ?(path = path) name =
-  System.fspathConcat
+  Filename.concat
     (List.find (fun dir ->
-       let p = System.fspathConcat dir name in
+       let p = Filename.concat dir name in
        let found = System.file_exists p in
-       debug (fun () -> Util.msg "'%s' ...%s\n"
-         (System.fspathToString p)
+       debug (fun () -> Util.msg "'%s' ...%s\n" p
          (match found with true -> "found" | false -> "not found"));
        found)
     path)
     name
 
-let exec_path = [System.fspathFromString Sys.executable_name]
+let exec_path = [Sys.executable_name]
 (*
   try
     (* Linux *)
@@ -258,13 +257,12 @@ let exec_path = [System.fspathFromString Sys.executable_name]
       [System.fspathConcat (System.getcwd ()) name]
 *)
 
-let exec_dir = List.map System.fspathDirname exec_path
+let exec_dir = List.map Filename.dirname exec_path
 
 let watcher =
   lazy
     (let suffix = if Util.osType = `Win32 then ".exe" else "" in
      debug (fun () -> Util.msg "File monitoring helper program...\n");
-     System.fspathToString
        (try
           search_in_path ~path:(exec_dir @ path)
             ("unison-fsmonitor-" ^ Uutil.myMajorVersion ^ suffix)
@@ -276,6 +274,7 @@ type 'a exn_option = Value of 'a | Exn of exn | Nothing
 
 type conn =
   { output : Lwt_unix.file_descr;
+    pid : int;
     has_changes : Cond.t;
     has_line : Cond.t;
     line_read : Cond.t;
@@ -295,6 +294,9 @@ let rec reader conn read_line =
     reader conn read_line
    end
 
+let safeTerm pid =
+  try ignore (Terminal.safe_waitpid pid) with Unix.Unix_error _ -> ()
+
 let safeClose fd = try Lwt_unix.close fd with Unix.Unix_error _ -> ()
 
 let currentConnection () =
@@ -304,7 +306,7 @@ let currentConnection () =
 
 let closeConnection () =
   match !conn with
-    Some c -> conn := None; safeClose c.output
+  | Some c -> conn := None; safeClose c.output; safeTerm c.pid
   | None   -> ()
 
 let connected () = !conn <> None
@@ -312,15 +314,14 @@ let connected () = !conn <> None
 let startProcess () =
   try
     let w = Lazy.force watcher in
-    let (i1,o1) = Lwt_unix.pipe_out () in
-    let (i2,o2) = Lwt_unix.pipe_in () in
-    Lwt_unix.set_close_on_exec i2;
-    Lwt_unix.set_close_on_exec o1;
-    Util.convertUnixErrorsToFatal "starting filesystem watcher" (fun () ->
-      ignore (System.create_process w [|w|] i1 o2 Unix.stderr));
+    let (i1,o1) = Lwt_unix.pipe_out ~cloexec:true () in
+    let (i2,o2) = Lwt_unix.pipe_in ~cloexec:true () in
+    let pid = Util.convertUnixErrorsToFatal "starting filesystem watcher"
+      (fun () -> System.create_process w [|w|] i1 o2 Unix.stderr) in
     Unix.close i1; Unix.close o2;
     let c =
       { output = o1;
+        pid;
         has_changes = Cond.make ();
         has_line = Cond.make ();
         line_read = Cond.make ();
@@ -443,6 +444,8 @@ let start hash =
     true
   end
 
+let running _ = connected ()
+
 let wait hash =
   let c = currentConnection () in
   let res = Cond.wait c.has_changes in
diff --git a/src/fswatch.mli b/src/fswatch.mli
index c56e28e..d635d5c 100644
--- a/src/fswatch.mli
+++ b/src/fswatch.mli
@@ -4,6 +4,7 @@
 type archiveHash = string
 
 val start : archiveHash -> bool
+val running : archiveHash -> bool
 
 val startScanning : archiveHash -> Fspath.t -> Path.local -> unit
 val stopScanning : unit -> unit
diff --git a/src/fswatchold.ml b/src/fswatchold.ml
index 008fddc..8491207 100644
--- a/src/fswatchold.ml
+++ b/src/fswatchold.ml
@@ -15,9 +15,6 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
-(* FIX: we should check that the child process has not died and
-   restart it if so... *)
-
 (* FIX: the names of the paths being watched should get included
    in the name of the watcher's state file *)
 
@@ -49,8 +46,8 @@ let watchercmd archHash root =
     let cmd = Printf.sprintf "python \"%s\" \"%s\" --outfile \"%s\" --statefile \"%s\" %s %s\n"
                 fsmonfile
                 root
-                (System.fspathToPrintString changefile)
-                (System.fspathToPrintString statefile)
+                changefile
+                statefile
                 (String.concat " " follow)
                 (String.concat " " paths) in
     debug (fun() -> Util.msg "watchercmd = %s\n" cmd);
@@ -59,8 +56,9 @@ let watchercmd archHash root =
 
 module StringSet= Set.Make (String)
 module RootMap = Map.Make (String)
-type watcherinfo = {file: System.fspath;
+type watcherinfo = {file: string;
                     mutable ch:in_channel option;
+                    proc: out_channel;
                     chars: Buffer.t;
                     mutable lines: string list}
 let watchers : watcherinfo RootMap.t ref = ref RootMap.empty
@@ -106,12 +104,29 @@ let readChanges wi =
       (* Wait for change file to be built *)
       debug (fun() -> Util.msg
         "Waiting for change file %s\n"
-        (System.fspathToPrintString wi.file))
+        wi.file)
     end
   else
     (* Watcher running and channel built: go ahead and read *)
     readAvailableLinesFromWatcher wi
 
+let watcherRunning archHash =
+  RootMap.mem archHash !watchers &&
+    let wi = RootMap.find archHash !watchers in
+    match Unix.waitpid [Unix.WNOHANG] (System.process_out_pid wi.proc) with
+    | (0, _) -> true
+    | _ | exception Unix.Unix_error (ECHILD, _, _) ->
+        watchers := RootMap.remove archHash !watchers;
+        begin
+          try ignore (System.close_process_out wi.proc)
+          with Unix.Unix_error _ -> ()
+        end;
+        begin match wi.ch with
+        | Some ch -> close_in_noerr ch
+        | None -> ()
+        end;
+        false
+
 let getChanges archHash =
   if StringSet.mem archHash !newWatchers then
     Fswatch.getChanges archHash
@@ -120,6 +135,7 @@ let getChanges archHash =
     readChanges wi;
     let res = wi.lines in
     wi.lines <- [];
+    ignore (watcherRunning archHash); (* Clean up if necessary *)
     List.map Path.fromString (trim_duplicates res)
   end
 
@@ -129,15 +145,15 @@ let start archHash fspath =
   else if Fswatch.start archHash then begin
     newWatchers := StringSet.add archHash !newWatchers;
     true
-  end else if not (RootMap.mem archHash !watchers) then begin
+  end else if not (watcherRunning archHash) then begin
     (* Watcher process not running *)
     match watchercmd archHash (Fspath.toString fspath) with
       Some (changefile,cmd) ->
         debug (fun() -> Util.msg
                  "Starting watcher on fspath %s\n"
                  (Fspath.toDebugString fspath));
-        let _ = System.open_process_out cmd in
-        let wi = {file = changefile; ch = None;
+        let proc = System.open_process_out cmd in
+        let wi = {file = changefile; ch = None; proc;
                   lines = []; chars = Buffer.create 80} in
         watchers := RootMap.add archHash wi !watchers;
         true
@@ -149,11 +165,24 @@ let start archHash fspath =
     true
   end
 
+let running archHash =
+  if StringSet.mem archHash !newWatchers then begin
+    if Fswatch.running archHash then true
+    else begin
+      newWatchers := StringSet.remove archHash !newWatchers;
+      false
+    end
+  end else false
+    ||
+  watcherRunning archHash
+
 let wait archHash =
   if StringSet.mem archHash !newWatchers then
     Fswatch.wait archHash
   else if not (RootMap.mem archHash !watchers) then
     raise (Util.Fatal "No file monitoring helper program found")
+  else if not (watcherRunning archHash) then
+    raise (Util.Fatal "File monitoring helper program not running")
   else begin
     let wi = RootMap.find archHash !watchers in
     let rec loop () =
@@ -161,7 +190,15 @@ let wait archHash =
       if wi.lines = [] then begin
         debug (fun() -> Util.msg "Sleeping for %d seconds...\n" watchinterval);
         Lwt.bind (Lwt_unix.sleep (float watchinterval)) (fun () ->
-        loop ())
+        if watcherRunning archHash then
+          loop ()
+        else
+          (* Instead of immediately restarting the watcher, the only sensible
+             thing to do is to do a full scan (which will happen automatically
+             if the update scanner notices that watcher is not running). We
+             don't know if any updates have been missed and can no longer rely
+             on the watcher only. *)
+          Lwt.return ())
       end else
         Lwt.return ()
     in
diff --git a/src/fswatchold.mli b/src/fswatchold.mli
index cb19564..a97aceb 100644
--- a/src/fswatchold.mli
+++ b/src/fswatchold.mli
@@ -1,4 +1,5 @@
 
 val start : string -> Fspath.t -> bool
+val running : string -> bool
 val getChanges : string -> Path.t list
 val wait : string -> unit Lwt.t
diff --git a/src/globals.ml b/src/globals.ml
index a1a5cfc..f1d9328 100644
--- a/src/globals.ml
+++ b/src/globals.ml
@@ -26,6 +26,7 @@ let debug = Trace.debug "globals"
 
 let rawroots =
   Prefs.createStringList "root"
+    ~category:(`Basic `Sync)
     "root of a replica (should be used exactly twice)"
     ("Each use of this preference names the root of one of the replicas "
      ^ "for Unison to synchronize.  Exactly two roots are needed, so normal "
@@ -44,21 +45,36 @@ let setRawRoots l = Prefs.set rawroots (Safelist.rev l)
 
 let rawRoots () = Safelist.rev (Prefs.read rawroots)
 
+let parsedClrootCache = ref []
+
+let parsedClRawRoots () =
+  let key = Prefs.read rawroots in
+  match List.assq_opt key !parsedClrootCache with
+  | Some x -> x
+  | None -> let x = Safelist.map Clroot.parseRoot (rawRoots ()) in
+            parsedClrootCache := (key, x) :: !parsedClrootCache; x
+
+let wrongNumRootsExn roots =
+  Util.Fatal (Printf.sprintf "Wrong number of roots: \
+    2 expected, but %d provided (%s)\n(Maybe you specified \
+    roots both on the command line and in the profile?)"
+    (Safelist.length roots)
+    (String.concat ", " roots))
+
 let rawRootPair () =
   match rawRoots () with
     [r1; r2] -> (r1, r2)
-  | _        -> assert false
+  | roots    -> raise (wrongNumRootsExn roots)
 
 let theroots = ref []
 
+let uninstallRoots () = theroots := []; parsedClrootCache := []
+
 open Lwt
 let installRoots termInteract =
+  let () = uninstallRoots () in (* Clear out potential old roots *)
   let roots = rawRoots () in
-  if Safelist.length roots <> 2 then
-    raise (Util.Fatal (Printf.sprintf
-      "Wrong number of roots: 2 expected, but %d provided (%s)\n(Maybe you specified roots both on the command line and in the profile?)"
-      (Safelist.length roots)
-      (String.concat ", " roots) ));
+  if Safelist.length roots <> 2 then raise (wrongNumRootsExn roots);
   Safelist.fold_right
     (fun r cont ->
        Remote.canonizeRoot r (Clroot.parseRoot r) termInteract
@@ -66,16 +82,26 @@ let installRoots termInteract =
        cont >>= (fun l ->
        return (r' :: l))))
     roots (return []) >>= (fun roots' ->
+  let () = match roots' with
+           | [r1; r2] when r1 = r2 ->
+               raise (Util.Fatal (Printf.sprintf
+                   ("That's no good, the roots appear to be the same! Here's "
+                 ^^ "what I found:\nFirst root: %s\nSecond root: %s")
+                   (Common.root2string r1) (Common.root2string r2)))
+           | _ -> ()
+  in
   theroots := roots';
-  return ())
+  Negotiate.features (Common.sortRoots roots') >>=
+  return)
 
 (* Alternate interface, should replace old interface eventually *)
 let installRoots2 () =
   debug (fun () -> Util.msg "Installing roots...");
+  let () = uninstallRoots () in (* Clear out potential old roots *)
   let roots = rawRoots () in
   theroots :=
     Safelist.map Remote.canonize ((Safelist.map Clroot.parseRoot) roots);
-  theroots := !theroots
+  Lwt.ignore_result (Negotiate.features (Common.sortRoots !theroots) >>= return)
 
 let roots () =
   match !theroots with
@@ -129,27 +155,9 @@ let allRootsMapWithWaitingAction f wa =
     (rootsInCanonicalOrder ()) >>= (fun l ->
       return (Safelist.map snd (reorderCanonicalListToUsersOrder l)))
 
-let replicaHostnames () =
-  Safelist.map
-    (function (Local, _) -> ""
-            | (Remote h,_) -> h)
-    (rootsList())
-
-let allHostsIter f =
-  let rec iter l =
-    match l with
-      [] ->
-        return ()
-    | root :: rem ->
-        f root >>= (fun () ->
-        iter rem)
-  in
-  iter (replicaHostnames ())
-
-let allHostsMap f = Safelist.map f (replicaHostnames())
-
 let paths =
   Prefs.create "path" []
+    ~category:(`Basic `Sync)
     "path to synchronize"
     ("When no \\verb|path| preference is given, Unison will simply synchronize "
      ^ "the two entire replicas, beginning from the given pair of roots.  "
@@ -160,6 +168,7 @@ let paths =
      ^ "are not regular expressions.")
     (fun oldpaths string -> Safelist.append oldpaths [Path.fromString string])
     (fun l -> Safelist.map Path.toString l)
+    Umarshal.(list Path.m)
 
 (* FIX: this does weird things in case-insensitive mode... *)
 let globPath lr p =
@@ -194,33 +203,36 @@ let expandWildcardPaths() =
 (*****************************************************************************)
 
 let propagatePrefsTo =
-  Remote.registerHostCmd
-    "installPrefs"
-    (fun prefs -> return (Prefs.load prefs))
+  Remote.registerRootCmdWithConnection
+    "installPrefs" Prefs.mdumpedPrefs Umarshal.unit
+    (fun conn prefs -> return (Prefs.load prefs (Remote.connectionVersion conn)))
 
 let propagatePrefs () =
-  let prefs = Prefs.dump() in
-  let toHost root =
-    match root with
-      (Local, _) -> return ()
-    | (Remote host,_) ->
-        propagatePrefsTo host prefs
+  let toRoot = function
+    | (Local, _) -> return ()
+    | (Remote _, _) as root ->
+        let rpcVer = Remote.(connectionVersion (connectionOfRoot root)) in
+        let prefs = Prefs.dump rpcVer in
+        propagatePrefsTo root root prefs
   in
-  allRootsIter toHost
+  allRootsIter toRoot
 
 (*****************************************************************************)
 (*                      PREFERENCES AND PREDICATES                           *)
 (*****************************************************************************)
 
 let batch =
-  Prefs.createBool "batch" false "batch mode: ask no questions at all"
+  Prefs.createBool "batch" false
+    ~category:(`Basic `Syncprocess)
+    "batch mode: ask no questions at all"
     ("When this is set to {\\tt true}, the user "
      ^ "interface will ask no questions at all.  Non-conflicting changes "
      ^ "will be propagated; conflicts will be skipped.")
 
 let confirmBigDeletes =
   Prefs.createBool "confirmbigdel" true
-    "!ask about whole-replica (or path) deletes"
+    ~category:(`Advanced `Syncprocess)
+    "ask about whole-replica (or path) deletes"
     ("When this is set to {\\tt true}, Unison will request an extra confirmation if it appears "
      ^ "that the entire replica has been deleted, before propagating the change.  If the {\\tt batch} "
      ^ "flag is also set, synchronization will be aborted.  When the {\\tt path} preference is used, "
@@ -231,6 +243,7 @@ let () = Prefs.alias confirmBigDeletes "confirmbigdeletes"
 
 let ignorePred =
   Pred.create "ignore"
+    ~category:(`Basic `Sync)
     ("Including the preference \\texttt{-ignore \\ARG{pathspec}} causes Unison to "
      ^ "completely ignore paths that match \\ARG{pathspec} (as well as their "
      ^ "children).  This is useful for avoiding synchronizing temporary "
@@ -241,6 +254,7 @@ let ignorePred =
 
 let ignorenotPred =
   Pred.create "ignorenot"
+    ~category:(`Basic `Sync)
     ("This preference overrides the preference \\texttt{ignore}.
       It gives a list of patterns
      (in the same format as
@@ -259,7 +273,9 @@ let ignorenotPred =
      synchronized will not work.  Instead, you should use the {\\tt path}
      preference to choose particular paths to synchronize.")
 
-let atomic = Pred.create "atomic" ~advanced:true
+let atomic = Pred.create "atomic"
+  ~category:(`Advanced `Sync)
+  ~local:true
   ("This preference specifies paths for directories whose "
    ^ "contents will be considered as a group rather than individually when "
    ^ "they are both modified.  "
@@ -276,7 +292,8 @@ let addRegexpToIgnore re =
   Pred.intern ignorePred newRE
 
 let merge =
-  Pred.create "merge" ~advanced:true
+  Pred.create "merge"
+    ~category:(`Advanced `Sync)
     ("This preference can be used to run a merge program which will create "
      ^ "a new version for each of the files and the backup, "
      ^ "with the last backup and both replicas. "
@@ -290,19 +307,25 @@ let shouldMerge p = Pred.test merge (Path.toString p)
 let mergeCmdForPath p = Pred.assoc merge (Path.toString p)
 
 let someHostIsRunningWindows =
-  Prefs.createBool "someHostIsRunningWindows" false "*" ""
+  Prefs.createBool "someHostIsRunningWindows" false
+    ~category:(`Internal `Pseudo)
+    "*" ""
 
 let allHostsAreRunningWindows =
-  Prefs.createBool "allHostsAreRunningWindows" false "*" ""
+  Prefs.createBool "allHostsAreRunningWindows" false
+    ~category:(`Internal `Pseudo)
+    "*" ""
 
 let fatFilesystem =
-  Prefs.createBool "fat" ~local:true false
+  Prefs.createBool "fat" false
+    ~category:(`Advanced `Syncprocess)
+    ~local:true
     "use appropriate options for FAT filesystems"
     ("When this is set to {\\tt true}, Unison will use appropriate options \
       to synchronize efficiently and without error a replica located on a \
       FAT filesystem on a non-Windows machine: \
       do not synchronize permissions ({\\tt perms = 0}); \
-      never use chmod ({\tt dontchmod = true}); \
+      never use chmod ({\\tt dontchmod = true}); \
       treat filenames as case insensitive ({\\tt ignorecase = true}); \
       do not attempt to synchronize symbolic links ({\\tt links = false}); \
       ignore inode number changes when detecting updates \
diff --git a/src/globals.mli b/src/globals.mli
index 440f12d..ba22673 100644
--- a/src/globals.mli
+++ b/src/globals.mli
@@ -10,12 +10,18 @@ val rawRoots : unit -> string list
 val setRawRoots : string list -> unit
 val rawRootPair : unit -> string * string
 
+(* Same as [rawRoots], parsed as clroots                                     *)
+val parsedClRawRoots : unit -> Clroot.clroot list
+
 (* Parse and canonize roots from their raw names                             *)
-val installRoots : (string -> string -> string) option -> unit Lwt.t
+val installRoots : (string -> Terminal.termInteract) option -> unit Lwt.t
 
 (* An alternate method (under development?) *)
 val installRoots2 : unit -> unit
 
+(* Clear previously installed roots; typically used when switching profiles  *)
+val uninstallRoots : unit -> unit
+
 (* The roots of the synchronization (with names canonized, but in the same   *)
 (* order as the user gave them)                                              *)
 val roots : unit -> Common.root * Common.root
@@ -55,12 +61,6 @@ val paths : Path.t list Prefs.t
 (* Expand any paths ending with *                                            *)
 val expandWildcardPaths : unit -> unit
 
-(* Run a command on all hosts in roots                                       *)
-val allHostsIter : (string -> unit Lwt.t) -> unit Lwt.t
-
-(* Run a command on all hosts in roots and collect results                   *)
-val allHostsMap : (string -> 'a) -> 'a list
-
 (* Make sure that the server has the same settings for its preferences as we
    do locally.  Should be called whenever the local preferences have
    changed.  (This isn't conceptually a part of this module, but it can't
diff --git a/src/hash_compat.c b/src/hash_compat.c
index 34221d1..152bb46 100644
--- a/src/hash_compat.c
+++ b/src/hash_compat.c
@@ -1,7 +1,10 @@
 /* The pre-OCaml 4.00 hash implementation */
 /* FIXME: This is included for backwards compatibility only and must be
- * REMVOED at next Unison version increase. The removal of this will
- * break Unison version compatibility. */
+ * REMOVED when a new hash function included in a stable release has been
+ * available for a few years. The removal of this function will break
+ * Unison version compatibility. There must be plenty of time given
+ * for users to upgrade (most users don't compile themselves and are at
+ * mercy of whatever package repositories they use). */
 
 /* Code copied from OCaml sources */
 /**************************************************************************/
@@ -19,17 +22,8 @@
 /*                                                                        */
 /**************************************************************************/
 
-#define CAML_NAME_SPACE
 #include <caml/mlvalues.h>
 #include <caml/custom.h>
-#ifndef Bytes_val /* Hack to know that we are on OCaml < 4.06.
-                     #include <caml/version.h> is not always found, for some reason. */
-extern value caml_hash_univ_param(value count, value limit, value obj);
-CAMLprim value unsn_hash_univ_param(value count, value limit, value obj)
-{
-  return caml_hash_univ_param(count, limit, obj);
-}
-#else
 #include <caml/address_class.h>
 
 struct hash_state {
@@ -169,4 +163,3 @@ static void hash_aux(struct hash_state* h, value obj)
       break;
     }
 }
-#endif
diff --git a/src/linkgtk2.ml b/src/linkgtk3.ml
similarity index 88%
rename from src/linkgtk2.ml
rename to src/linkgtk3.ml
index ddc889d..c527ae4 100644
--- a/src/linkgtk2.ml
+++ b/src/linkgtk3.ml
@@ -1,4 +1,4 @@
-(* Unison file synchronizer: src/linkgtk2.ml *)
+(* Unison file synchronizer: src/linkgtk3.ml *)
 (* Copyright 1999-2020, Benjamin C. Pierce
 
     This program is free software: you can redistribute it and/or modify
@@ -16,4 +16,4 @@
 *)
 
 
-module TopLevel = Main.Body(Uigtk2.Body)
+module TopLevel = Main.Body(Uigtk3.Body)
diff --git a/src/lock.ml b/src/lock.ml
index 99c8e14..5c22e33 100644
--- a/src/lock.ml
+++ b/src/lock.ml
@@ -33,7 +33,7 @@ let create name mode =
     false
 
 let rec unique name i mode =
-  let nm = System.fspathAddSuffixToFinalName name (string_of_int i) in
+  let nm = name ^ (string_of_int i) in
   if create nm mode then nm else
     (* highly unlikely *)
     unique name (i + 1) mode
diff --git a/src/lock.mli b/src/lock.mli
index 2006e65..4f9ee40 100644
--- a/src/lock.mli
+++ b/src/lock.mli
@@ -4,6 +4,6 @@
 (* A simple utility module for setting and releasing inter-process locks
    using entries in the filesystem. *)
 
-val acquire : System.fspath -> bool
-val release : System.fspath -> unit
-val is_locked : System.fspath -> bool
+val acquire : string -> bool
+val release : string -> unit
+val is_locked : string -> bool
diff --git a/src/lwt/META b/src/lwt/META
deleted file mode 100644
index 9b6acde..0000000
--- a/src/lwt/META
+++ /dev/null
@@ -1,4 +0,0 @@
-requires = "unix"
-version = "0.1"
-archive(byte) = "lwt.cma"
-archive(native) = "lwt.cmxa"
diff --git a/src/lwt/Makefile b/src/lwt/Makefile
deleted file mode 100644
index 3cee865..0000000
--- a/src/lwt/Makefile
+++ /dev/null
@@ -1,55 +0,0 @@
-
-NAME = lwt
-
-OCAMLC = ocamlfind ocamlc -g
-OCAMLOPT = ocamlfind ocamlopt
-OCAMLDEP = ocamldep
-
-OBJECTS  = pqueue.cmo lwt.cmo lwt_util.cmo lwt_unix.cmo
-XOBJECTS = $(OBJECTS:cmo=cmx)
-
-ARCHIVE  = $(NAME).cma
-XARCHIVE = $(NAME).cmxa
-
-REQUIRES =
-PREDICATES =
-
-all: $(ARCHIVE) 
-opt: $(XARCHIVE)
-
-$(ARCHIVE): $(OBJECTS)
-	$(OCAMLC) -a -o $(ARCHIVE) -package "$(REQUIRES)" -linkpkg \
-	          -predicates "$(PREDICATES)" $(OBJECTS)
-$(XARCHIVE): $(XOBJECTS)
-	$(OCAMLOPT) -a -o $(XARCHIVE) -package "$(REQUIRES)" -linkpkg \
-	          -predicates "$(PREDICATES)" $(XOBJECTS)
-
-.SUFFIXES: .cmo .cmi .cmx .ml .mli
-
-.ml.cmo:
-	$(OCAMLC) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-.mli.cmi:
-	$(OCAMLC) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-.ml.cmx:
-	$(OCAMLOPT) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-
-depend: *.ml *.mli
-	$(OCAMLDEP) *.ml *.mli > depend
-include depend
-
-install: all
-	{ test ! -f $(XARCHIVE) || extra="$(XARCHIVE) "`basename $(XARCHIVE) .cmxa`.a; }; \
-	ocamlfind install $(NAME) *.mli *.cmi $(ARCHIVE) META $$extra
-
-uninstall:
-	ocamlfind remove $(NAME)
-
-clean::
-	$(RM) -f *.cmi *.cmo *.cmx *.cma *.cmxa *.a *.o *~ *.bak
-	$(RM) -f win/*.cm[ioxa] win/*.cmxa win/*.a win/*.o win/*~ win/*.bak
-
-clean::
-	cd example && $(MAKE) clean
diff --git a/src/lwt/depend b/src/lwt/depend
deleted file mode 100644
index cf0dfde..0000000
--- a/src/lwt/depend
+++ /dev/null
@@ -1,12 +0,0 @@
-lwt.cmo : lwt.cmi
-lwt.cmx : lwt.cmi
-lwt.cmi :
-lwt_unix.cmo : lwt_unix.cmi
-lwt_unix.cmx : lwt_unix.cmi
-lwt_unix.cmi : lwt.cmi
-lwt_util.cmo : lwt.cmi lwt_util.cmi
-lwt_util.cmx : lwt.cmx lwt_util.cmi
-lwt_util.cmi : lwt.cmi
-pqueue.cmo : pqueue.cmi
-pqueue.cmx : pqueue.cmi
-pqueue.cmi :
diff --git a/src/lwt/example/Makefile b/src/lwt/example/Makefile
deleted file mode 100644
index 766efa5..0000000
--- a/src/lwt/example/Makefile
+++ /dev/null
@@ -1,12 +0,0 @@
-all: relay start_editor
-
-OCAMLC = ocamlfind ocamlc
-
-relay: relay.ml
-	$(OCAMLC) -o relay -linkpkg -package lwt relay.ml -cclib -s
-
-start_editor : editor.ml
-	$(OCAMLC) -o start_editor -linkpkg -package lwt editor.ml -cclib -s
-
-clean:
-	rm -f *.cmi *.cmo *~ relay start_editor
diff --git a/src/lwt/example/editor.ml b/src/lwt/example/editor.ml
deleted file mode 100644
index 0995528..0000000
--- a/src/lwt/example/editor.ml
+++ /dev/null
@@ -1,3 +0,0 @@
-let _ =
-  let editor = try Sys.getenv "EDITOR" with Not_found -> "emacs" in
-  Lwt_unix.run (Lwt_unix.system editor)
diff --git a/src/lwt/example/relay.ml b/src/lwt/example/relay.ml
deleted file mode 100644
index 9cdd337..0000000
--- a/src/lwt/example/relay.ml
+++ /dev/null
@@ -1,55 +0,0 @@
-
-(* Usage: relay <listening_port> <dest_port> *)
-
-(* This program waits for a connection on <listening_port>. It then
-   connect to <dest_port> and relay everything it receives in either
-   side to the other side.  It exists when either side closes the
-   connection. *)
-
-let listening_port = int_of_string Sys.argv.(1)
-let dest_port = int_of_string Sys.argv.(2)
-
-open Lwt
-
-let rec really_write out_ch buffer pos len =
-  Lwt_unix.write out_ch buffer pos len >>= (fun len' ->
-  if len = len' then return () else
-  really_write out_ch buffer (pos + len') (len - len'))
-
-let relay in_ch out_ch =
-  let rec relay_rec previous_write =
-    let buffer = Bytes.create 8192 in
-    (* Read some data from the input socket *)
-    Lwt_unix.read in_ch buffer 0 8192 >>= (fun len ->
-    (* If we read nothing, this means that the connection has been
-       closed.  In this case, we stop relaying. *)
-    if len = 0 then return () else begin
-      (* Otherwise, we write the data to the ouput socket *)
-      let write =
-        (* First wait for the previous write to terminate *)
-        previous_write >>= (fun () ->
-        (* Then write the contents of the buffer *)
-        really_write out_ch buffer 0 len)
-      in
-      relay_rec write
-    end)
-  in
-  relay_rec (return ())
-
-let new_socket () = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0
-let local_addr num = Unix.ADDR_INET (Unix.inet_addr_any, num)
-
-let _ =
-  Lwt_unix.run
-    ((* Initialize the listening address *)
-     new_socket () >>= (fun listening_socket ->
-     Unix.setsockopt listening_socket Unix.SO_REUSEADDR true;
-     Unix.bind listening_socket (local_addr listening_port);
-     Unix.listen listening_socket 1;
-     (* Wait for a connection *)
-     Lwt_unix.accept listening_socket >>= (fun (inp, _) ->
-     (* Connect to the destination port *)
-     new_socket () >>= (fun out ->
-     Lwt_unix.connect out (local_addr dest_port) >>= (fun () ->
-     (* Start relaying *)
-     Lwt.choose [relay inp out; relay out inp])))))
diff --git a/src/lwt/generic/lwt_unix_impl.ml b/src/lwt/generic/lwt_unix_impl.ml
index 238ce04..9ebf761 100644
--- a/src/lwt/generic/lwt_unix_impl.ml
+++ b/src/lwt/generic/lwt_unix_impl.ml
@@ -1,6 +1,6 @@
 (*
 Non-blocking I/O and select does not (fully) work under Windows.
-The libray therefore does not use them under Windows, and will
+The library therefore does not use them under Windows, and will
 therefore have the following limitations:
 - No read will be performed while there are some threads ready to run
   or waiting to write;
@@ -14,9 +14,6 @@ therefore have the following limitations:
 - [connect] is blocking
 *)
 let windows_hack = Sys.os_type <> "Unix"
-let recent_ocaml =
-  Scanf.sscanf Sys.ocaml_version "%d.%d"
-    (fun maj min -> (maj = 3 && min >= 11) || maj > 3)
 
 module SleepQueue =
   Pqueue.Make (struct
@@ -63,12 +60,6 @@ let of_unix_file_descr fd = if not windows_hack then Unix.set_nonblock fd; fd
 
 let inputs = ref []
 let outputs = ref []
-let wait_children = ref []
-
-let child_exited = ref false
-let _ =
-  if not windows_hack then
-    ignore(Sys.signal Sys.sigchld (Sys.Signal_handle (fun _ -> child_exited := true)))
 
 let bad_fd fd =
   try ignore (Unix.LargeFile.fstat fd); false with
@@ -82,9 +73,6 @@ let wrap_syscall queue fd cont syscall =
     with
       Exit
     | Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK | Unix.EINTR), _, _) ->
-        (* EINTR because we are catching SIG_CHLD hence the system call
-           might be interrupted to handle the signal; this lets us restart
-           the system call eventually. *)
         None
     | e ->
         queue := List.remove_assoc fd !queue;
@@ -119,12 +107,7 @@ let rec run thread =
       let infds = List.map fst !inputs in
       let outfds = List.map fst !outputs in
       let (readers, writers, _) =
-        if windows_hack && not recent_ocaml then
-          let writers = outfds in
-          let readers =
-            if delay = 0. || writers <> [] then [] else infds in
-          (readers, writers, [])
-        else if infds = [] && outfds = [] && delay = 0. then
+        if infds = [] && outfds = [] && delay = 0. then
           ([], [], [])
         else
           try
@@ -138,11 +121,6 @@ let rec run thread =
               ([], [], [])
           | Unix.Unix_error (Unix.EBADF, _, _) ->
               (List.filter bad_fd infds, List.filter bad_fd outfds, [])
-          | Unix.Unix_error (Unix.EPIPE, _, _)
-            when windows_hack && recent_ocaml ->
-            (* Workaround for a bug in Ocaml 3.11: select fails with an
-               EPIPE error when the file descriptor is remotely closed *)
-              (infds, [], [])
       in
       restart_threads !event_counter now;
       List.iter
@@ -155,7 +133,7 @@ let rec run thread =
              | `Accept res ->
                   wrap_syscall inputs fd res
                     (fun () ->
-                       let (s, _) as v = Unix.accept fd in
+                       let (s, _) as v = Unix.accept ~cloexec:true fd in
                        if not windows_hack then Unix.set_nonblock s;
                        v)
              | `Wait res ->
@@ -184,17 +162,6 @@ let rec run thread =
            with Not_found ->
              ())
         writers;
-      if !child_exited then begin
-        child_exited := false;
-        List.iter
-          (fun (id, (res, flags, pid)) ->
-             wrap_syscall wait_children id res
-               (fun () ->
-                  let (pid', _) as v = Unix.waitpid flags pid in
-                  if pid' = 0 then raise Exit;
-                  v))
-          !wait_children
-      end;
       run thread
 
 (****)
@@ -223,8 +190,7 @@ let read ch buf pos len =
 
 let write ch buf pos len =
   try
-    if windows_hack && recent_ocaml then
-      raise (Unix.Unix_error (Unix.EAGAIN, "", ""));
+    if windows_hack then raise (Unix.Unix_error (Unix.EAGAIN, "", ""));
     Lwt.return (Unix.write ch buf pos len)
   with
     Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK), _, _) ->
@@ -236,8 +202,7 @@ let write ch buf pos len =
 
 let write_substring ch buf pos len =
   try
-    if windows_hack && recent_ocaml then
-      raise (Unix.Unix_error (Unix.EAGAIN, "", ""));
+    if windows_hack then raise (Unix.Unix_error (Unix.EAGAIN, "", ""));
     Lwt.return (Unix.write_substring ch buf pos len)
   with
     Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK), _, _) ->
@@ -247,30 +212,20 @@ let write_substring ch buf pos len =
   | e ->
       Lwt.fail e
 
-(*
-let pipe () =
-  let (in_fd, out_fd) as fd_pair = Unix.pipe() in
-  if not windows_hack then begin
-    Unix.set_nonblock in_fd;
-    Unix.set_nonblock out_fd
-  end;
-  fd_pair
-*)
-
-let pipe_in () =
-  let (in_fd, out_fd) as fd_pair = Unix.pipe() in
+let pipe_in ?cloexec () =
+  let (in_fd, out_fd) as fd_pair = Unix.pipe ?cloexec () in
   if not windows_hack then
     Unix.set_nonblock in_fd;
   fd_pair
 
-let pipe_out () =
-  let (in_fd, out_fd) as fd_pair = Unix.pipe() in
+let pipe_out ?cloexec () =
+  let (in_fd, out_fd) as fd_pair = Unix.pipe ?cloexec () in
   if not windows_hack then
     Unix.set_nonblock out_fd;
   fd_pair
 
-let socket dom typ proto =
-  let s = Unix.socket dom typ proto in
+let socket ?cloexec dom typ proto =
+  let s = Unix.socket ?cloexec dom typ proto in
   if not windows_hack then Unix.set_nonblock s;
   s
 
@@ -308,35 +263,6 @@ let connect s addr =
   | e ->
       Lwt.fail e
 
-let ids = ref 0
-let new_id () = incr ids; !ids
-
-let _waitpid flags pid =
-  try
-    Lwt.return (Unix.waitpid flags pid)
-  with e ->
-    Lwt.fail e
-
-let waitpid flags pid =
-  if List.mem Unix.WNOHANG flags || windows_hack then
-    _waitpid flags pid
-  else
-    let flags = Unix.WNOHANG :: flags in
-    Lwt.bind (_waitpid flags pid) (fun ((pid', _) as res) ->
-    if pid' <> 0 then
-      Lwt.return res
-    else
-      let res = Lwt.wait () in
-      wait_children := (new_id (), (res, flags, pid)) :: !wait_children;
-      res)
-
-let wait () = waitpid [] (-1)
-
-let system cmd =
-  match Unix.fork () with
-     0 -> Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
-  | id -> Lwt.bind (waitpid [] id) (fun (pid, status) -> Lwt.return status)
-
 (****)
 
 type lwt_in_channel = in_channel
@@ -361,31 +287,6 @@ let rec input_char ic =
   | e ->
       Lwt.fail e
 
-let stdlib_input = input
-let rec input ic s ofs len =
-  try
-    Lwt.return (stdlib_input ic s ofs len)
-  with
-    Sys_blocked_io ->
-      Lwt.bind (wait_inchan ic) (fun () -> input ic s ofs len)
-  | e ->
-      Lwt.fail e
-
-let rec unsafe_really_input ic s ofs len =
-  if len <= 0 then
-    Lwt.return ()
-  else begin
-    Lwt.bind (input ic s ofs len) (fun r ->
-    if r = 0
-    then Lwt.fail End_of_file
-    else unsafe_really_input ic s (ofs+r) (len-r))
-  end
-
-let really_input ic s ofs len =
-  if ofs < 0 || len < 0 || ofs > Bytes.length s - len
-  then Lwt.fail (Invalid_argument "really_input")
-  else unsafe_really_input ic s ofs len
-
 let input_line ic =
   let buf = ref (Bytes.create 128) in
   let pos = ref 0 in
@@ -416,113 +317,3 @@ let input_line ic =
        let res = Bytes.create !pos in
        Bytes.blit !buf 0 res 0 !pos;
        Lwt.return (Bytes.to_string res))
-
-(****)
-
-type popen_process =
-    Process of in_channel * out_channel
-  | Process_in of in_channel
-  | Process_out of out_channel
-  | Process_full of in_channel * out_channel * in_channel
-
-let popen_processes = (Hashtbl.create 7 : (popen_process, int) Hashtbl.t)
-
-let open_proc cmd proc input output toclose =
-  match Unix.fork () with
-     0 -> if input <> Unix.stdin then begin
-            Unix.dup2 input Unix.stdin;
-            Unix.close input
-          end;
-          if output <> Unix.stdout then begin
-            Unix.dup2 output Unix.stdout;
-            Unix.close output
-          end;
-          List.iter Unix.close toclose;
-          Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
-  | id -> Hashtbl.add popen_processes proc id
-
-let open_process_in cmd =
-  let (in_read, in_write) = pipe_in () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  open_proc cmd (Process_in inchan) Unix.stdin in_write [in_read];
-  Unix.close in_write;
-  Lwt.return inchan
-
-let open_process_out cmd =
-  let (out_read, out_write) = pipe_out () in
-  let outchan = Unix.out_channel_of_descr out_write in
-  open_proc cmd (Process_out outchan) out_read Unix.stdout [out_write];
-  Unix.close out_read;
-  Lwt.return outchan
-
-let open_process cmd =
-  let (in_read, in_write) = pipe_in () in
-  let (out_read, out_write) = pipe_out () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  let outchan = Unix.out_channel_of_descr out_write in
-  open_proc cmd (Process(inchan, outchan)) out_read in_write
-                                           [in_read; out_write];
-  Unix.close out_read;
-  Unix.close in_write;
-  Lwt.return (inchan, outchan)
-
-(* FIX: Subprocesses that use /dev/tty to print things on the terminal
-   will NOT have this output captured and returned to the caller of this
-   function.  There's an argument that this is correct, but if we are
-   running from a GUI the user may not be looking at any terminal and it
-   will appear that the process is just hanging.  This can be fixed, in
-   principle, by writing a little C code that opens /dev/tty and then uses
-   the TIOCNOTTY ioctl control to detach the terminal. *)
-
-let open_proc_full cmd env proc input output error toclose =
-  match Unix.fork () with
-     0 -> Unix.dup2 input Unix.stdin; Unix.close input;
-          Unix.dup2 output Unix.stdout; Unix.close output;
-          Unix.dup2 error Unix.stderr; Unix.close error;
-          List.iter Unix.close toclose;
-          Unix.execve "/bin/sh" [| "/bin/sh"; "-c"; cmd |] env
-  | id -> Hashtbl.add popen_processes proc id
-
-let open_process_full cmd env =
-  let (in_read, in_write) = pipe_in () in
-  let (out_read, out_write) = pipe_out () in
-  let (err_read, err_write) = pipe_in () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  let outchan = Unix.out_channel_of_descr out_write in
-  let errchan = Unix.in_channel_of_descr err_read in
-  open_proc_full cmd env (Process_full(inchan, outchan, errchan))
-                 out_read in_write err_write [in_write; out_read; err_read];
-  Unix.close out_read;
-  Unix.close in_write;
-  Unix.close err_write;
-  Lwt.return (inchan, outchan, errchan)
-
-let find_proc_id fun_name proc =
-  try
-    let pid = Hashtbl.find popen_processes proc in
-    Hashtbl.remove popen_processes proc;
-    pid
-  with Not_found ->
-    raise (Unix.Unix_error (Unix.EBADF, fun_name, ""))
-
-let close_process_in inchan =
-  let pid = find_proc_id "close_process_in" (Process_in inchan) in
-  close_in inchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process_out outchan =
-  let pid = find_proc_id "close_process_out" (Process_out outchan) in
-  close_out outchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process (inchan, outchan) =
-  let pid = find_proc_id "close_process" (Process(inchan, outchan)) in
-  close_in inchan; close_out outchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process_full (outchan, inchan, errchan) =
-  let pid =
-    find_proc_id "close_process_full"
-                 (Process_full(outchan, inchan, errchan)) in
-  close_out inchan; close_in outchan; close_in errchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
diff --git a/src/lwt/lwt.ml b/src/lwt/lwt.ml
index aa84933..a273702 100644
--- a/src/lwt/lwt.ml
+++ b/src/lwt/lwt.ml
@@ -53,20 +53,6 @@ let rec connect t t' =
     end
   end
 
-(* similar to [connect t t']; does nothing instead of raising exception when
- * [t] is not asleep
- *)
-let rec try_connect t t' =
-  if t.state <> Sleep then
-    ()
-  else if t'.state = Sleep then
-    add_waiter t' (fun () -> try_connect t t')
-  else begin
-    t.state <- t'.state;
-    List.iter (fun f -> f ()) t.waiters;
-    t.waiters <- []
-  end
-
 (* apply function, reifying explicit exceptions into the thread type
  * apply: ('a -(exn)-> 'b t) -> ('a -(n)-> 'b t)
  * semantically a natural transformation TE -> T, where T is the thread
@@ -155,6 +141,19 @@ let choose l =
     nth_ready l (Random.int !ready)
   else
     let res = wait () in
-    (* XXX We may leak memory here, if we repeatedly select the same event *)
-    List.iter (fun x -> try_connect res x) l;
+    (* All waiters for this [choose] need to be remembered and cleared
+       out once one of the threads finishes, to not leak memory. *)
+    let waits = ref [] in
+    let choose_done x =
+      List.iter (fun (t, waiter) ->
+          t.waiters <- List.filter (fun f -> f !=(*phys*) waiter) t.waiters)
+        !waits;
+      connect res x
+    in
+    let remember_waiter x =
+      let waiter () = choose_done x in
+      waits := (x, waiter) :: !waits;
+      waiter
+    in
+    List.iter (fun x -> remember_waiter x |> add_waiter x) l;
     res
diff --git a/src/lwt/lwt.mli b/src/lwt/lwt.mli
index 9cb84cb..c1f3c7d 100644
--- a/src/lwt/lwt.mli
+++ b/src/lwt/lwt.mli
@@ -41,12 +41,12 @@ val try_bind : (unit -> 'a t) -> ('a -> 'b t) -> (exn -> 'b t) -> 'b t
 
 val choose : 'a t list -> 'a t
       (* [choose l] behaves as the first thread in [l] to terminate.
-         If several threads are already terminated, one is choosen
+         If several threads are already terminated, one is chosen
          at random. *)
 
 val ignore_result : 'a t -> unit
       (* [ignore_result t] start the thread [t] and ignores its result
-         value if the thread terminates sucessfully.  However, if the
+         value if the thread terminates successfully.  However, if the
          thread [t] fails, the exception is raised instead of being
          ignored.
          You should use this function if you want to start a thread
diff --git a/src/lwt/lwt_unix.mli b/src/lwt/lwt_unix.mli
index a29a6c8..61860e0 100644
--- a/src/lwt/lwt_unix.mli
+++ b/src/lwt/lwt_unix.mli
@@ -39,10 +39,10 @@ val write : file_descr -> bytes -> int -> int -> int Lwt.t
 val write_substring : file_descr -> string -> int -> int -> int Lwt.t
 val wait_read : file_descr -> unit Lwt.t
 val wait_write : file_descr -> unit Lwt.t
-val pipe_in : unit -> file_descr * Unix.file_descr
-val pipe_out : unit -> Unix.file_descr * file_descr
+val pipe_in : ?cloexec:bool -> unit -> file_descr * Unix.file_descr
+val pipe_out : ?cloexec:bool -> unit -> Unix.file_descr * file_descr
 val socket :
-  Unix.socket_domain -> Unix.socket_type -> int -> file_descr
+  ?cloexec:bool -> Unix.socket_domain -> Unix.socket_type -> int -> file_descr
 val bind : file_descr -> Unix.sockaddr -> unit
 val setsockopt : file_descr -> Unix.socket_bool_option -> bool -> unit
 val accept : file_descr -> (file_descr * Unix.sockaddr) Lwt.t
diff --git a/src/lwt/lwt_unix_stubs.c b/src/lwt/lwt_unix_stubs.c
index 256d868..081f7d0 100644
--- a/src/lwt/lwt_unix_stubs.c
+++ b/src/lwt/lwt_unix_stubs.c
@@ -9,13 +9,26 @@
 #include <caml/fail.h>
 #include <caml/bigarray.h>
 #include <caml/callback.h>
+#include <caml/unixsupport.h>
+#include <caml/socketaddr.h>
+#include <caml/version.h>
+#if OCAML_VERSION < 41300
+#define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */
+#endif
+#include <caml/osdeps.h>
+
+#if OCAML_VERSION_MAJOR < 5
+#define caml_unix_cloexec_p unix_cloexec_p
+#define caml_unix_error_of_code unix_error_of_code
+#define caml_uerror uerror
+#define caml_win32_maperr win32_maperr
+#define caml_win32_alloc_handle win_alloc_handle
+#define caml_win32_alloc_socket win_alloc_socket
+#endif
 
 //#define D(x) x
 #define D(x) while(0){}
 
-#define UNIX_BUFFER_SIZE 16384
-#define Nothing ((value) 0)
-
 typedef struct
 {
   OVERLAPPED overlapped;
@@ -23,57 +36,36 @@ typedef struct
   long action;
 } completionData;
 
-struct filedescr {
-  union {
-    HANDLE handle;
-    SOCKET socket;
-  } fd;
-  enum { KIND_HANDLE, KIND_SOCKET } kind;
-  int crt_fd;
-};
-#define Handle_val(v) (((struct filedescr *) Data_custom_val(v))->fd.handle)
-#define Socket_val(v) (((struct filedescr *) Data_custom_val(v))->fd.socket)
-
-extern void win32_maperr (DWORD errcode);
-extern void uerror (char * cmdname, value arg);
-extern value unix_error_of_code (int errcode);
-extern value win_alloc_handle (HANDLE h);
-extern value win_alloc_socket(SOCKET);
-extern void get_sockaddr (value mladdr,
-                          struct sockaddr * addr /*out*/,
-                          int * addr_len /*out*/);
-
-#define Array_data(a, i) (((char *) a->data) + Long_val(i))
+#define Array_data(a, i) (((char *) Caml_ba_data_val(a)) + Long_val(i))
 
 #ifndef Bytes_val
 #define Bytes_val(x) ((unsigned char *) Bp_val(x))
 #endif
 
-CAMLprim value ml_blit_string_to_buffer
+CAMLprim value ml_blit_bytes_to_buffer
 (value s, value i, value a, value j, value l)
 {
-  const char *src = String_val(s) + Int_val(i);
-  char *dest = Array_data(Bigarray_val(a), j);
+  CAMLparam5(s, i, a, j, l);
+  unsigned char *src = Bytes_val(s) + Long_val(i);
+  char *dest = Array_data(a, j);
   memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  CAMLreturn(Val_unit);
 }
 
-CAMLprim value ml_blit_bytes_to_buffer
+CAMLprim value ml_blit_string_to_buffer
 (value s, value i, value a, value j, value l)
 {
-  char *src = Bytes_val(s) + Int_val(i);
-  char *dest = Array_data(Bigarray_val(a), j);
-  memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  return ml_blit_bytes_to_buffer(s, i, a, j, l);
 }
 
 CAMLprim value ml_blit_buffer_to_bytes
 (value a, value i, value s, value j, value l)
 {
-  char *src = Array_data(Bigarray_val(a), i);
-  char *dest = Bytes_val(s) + Long_val(j);
+  CAMLparam5(a, i, s, j, l);
+  char *src = Array_data(a, i);
+  unsigned char *dest = Bytes_val(s) + Long_val(j);
   memcpy(dest, src, Long_val(l));
-  return Val_unit;
+  CAMLreturn(Val_unit);
 }
 
 /****/
@@ -98,10 +90,10 @@ static void invoke_completion_callback
   err = Val_long(0);
   if (errCode != NO_ERROR) {
     len = -1;
-    win32_maperr (errCode);
-    err = unix_error_of_code(errno);
+    caml_win32_maperr(errCode);
+    err = caml_unix_error_of_code(errno);
   }
-  name = copy_string (action_name[action]);
+  name = caml_copy_string(action_name[action]);
   D(printf("Action %s completed: id %ld -> len %ld / err %d (errCode %ld)\n",
            action_name[action], id, len, errno, errCode));
   args[0] = Val_long(id);
@@ -110,6 +102,7 @@ static void invoke_completion_callback
   args[3] = name;
   caml_callbackN(completionCallback, 4, args);
   D(printf("Callback performed\n"));
+  CAMLreturn0;
 }
 
 typedef struct {
@@ -175,8 +168,8 @@ static HANDLE get_helper_thread (value threads, int kind) {
 
   h = CreateThread (NULL, 0, helper_thread, NULL, 0, NULL);
   if (h == NULL) {
-    win32_maperr (GetLastError ());
-    uerror("createHelperThread", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("createHelperThread", Nothing);
   }
   Field(threads, kind) = (value) h;
   return h;
@@ -256,11 +249,10 @@ static VOID CALLBACK perform_io_on_thread(ULONG_PTR param) {
 
 static void thread_io
 (long action, long id, value threads, HANDLE h, char * buf, long len) {
-  struct caml_bigarray *buf_arr = Bigarray_val(buf);
   ioInfo * info = GlobalAlloc(GPTR, sizeof(ioInfo));
   if (info == NULL) {
     errno = ENOMEM;
-    uerror(action_name[action], Nothing);
+    caml_uerror(action_name[action], Nothing);
   }
 
   info->action = action;
@@ -289,7 +281,7 @@ static void overlapped_action(long action, long id,
   completionData * d = GlobalAlloc(GPTR, sizeof(completionData));
   if (d == NULL) {
     errno = ENOMEM;
-    uerror(action_name[action], Nothing);
+    caml_uerror(action_name[action], Nothing);
   }
   d->id = id;
   d->action = action;
@@ -303,10 +295,10 @@ static void overlapped_action(long action, long id,
   if (!res) {
     err = GetLastError ();
     if (err != ERROR_IO_PENDING) {
-      win32_maperr (err);
+      caml_win32_maperr(err);
   D(printf("Action %s failed: id %ld -> err %d (errCode %ld)\n",
            action_name[action], id, errno, err));
-      uerror("ReadFileEx", Nothing);
+      caml_uerror("ReadFileEx", Nothing);
     }
   }
 }
@@ -327,29 +319,27 @@ CAMLprim value win_wrap_overlapped (value fd) {
 
 CAMLprim value win_read
 (value fd, value buf, value ofs, value len, value id) {
-  CAMLparam4(fd, buf, ofs, len);
-  struct caml_bigarray *buf_arr = Bigarray_val(buf);
+  CAMLparam5(fd, buf, ofs, len, id);
 
   if (Field(fd, 1) == Val_long(0))
     overlapped_action (READ_OVERLAPPED, Long_val(id), Handle(fd),
-                       Array_data (buf_arr, ofs), Long_val(len));
+                       Array_data(buf, ofs), Long_val(len));
   else
     thread_io (READ, Long_val(id), Field(fd, 1), Handle(fd),
-               Array_data (buf_arr, ofs), Long_val(len));
+               Array_data(buf, ofs), Long_val(len));
   CAMLreturn (Val_unit);
 }
 
 CAMLprim value win_write
 (value fd, value buf, value ofs, value len, value id) {
-  CAMLparam4(fd, buf, ofs, len);
-  struct caml_bigarray *buf_arr = Bigarray_val(buf);
+  CAMLparam5(fd, buf, ofs, len, id);
 
   if (Field(fd, 1) == Val_long(0))
     overlapped_action (WRITE_OVERLAPPED, Long_val(id), Handle(fd),
-                       Array_data (buf_arr, ofs), Long_val(len));
+                       Array_data(buf, ofs), Long_val(len));
   else
     thread_io (WRITE, Long_val(id), Field(fd, 1), Handle(fd),
-               Array_data (buf_arr, ofs), Long_val(len));
+               Array_data(buf, ofs), Long_val(len));
   CAMLreturn (Val_unit);
 }
 
@@ -369,46 +359,47 @@ static void after_connect (SOCKET s) {
 static HANDLE events[MAXIMUM_WAIT_OBJECTS];
 //static OVERLAPPED oData[MAXIMUM_WAIT_OBJECTS];
 
-CAMLprim value win_register_wait (value socket, value kind, value idx) {
-  CAMLparam3(socket, kind, idx);
-  long i = Long_val(idx);
+CAMLprim value win_register_wait (value socket, value kind) {
+  CAMLparam2(socket, kind);
+  HANDLE h;
   long mask;
 
-  D(printf("Register: i %ld, kind %ld\n", Long_val(i), Long_val(kind)));
-  events[i] = CreateEvent(NULL, TRUE, FALSE, NULL);
+  D(printf("Register: %lx, kind %ld\n", (long)(Socket_val(socket)), Long_val(kind)));
+  h = CreateEvent(NULL, TRUE, FALSE, NULL);
   mask = (Long_val(kind) == 0) ? FD_CONNECT : FD_ACCEPT;
-  if (WSAEventSelect(Socket_val(socket), events[i], mask) == SOCKET_ERROR) {
-    win32_maperr(WSAGetLastError ());
-    uerror("WSAEventSelect", Nothing);
+  if (WSAEventSelect(Socket_val(socket), h, mask) == SOCKET_ERROR) {
+    caml_win32_maperr(WSAGetLastError());
+    caml_uerror("WSAEventSelect", Nothing);
   }
 
-  CAMLreturn (Val_unit);
+  CAMLreturn(caml_win32_alloc_handle(h));
 }
 
-CAMLprim value win_check_connection (value socket, value kind, value idx) {
-  CAMLparam3 (socket, kind, idx);
+CAMLprim value win_check_connection (value socket, value kind, value h) {
+  CAMLparam3 (socket, kind, h);
   WSANETWORKEVENTS evs;
-  int res, err, i = Long_val(idx);
+  int res, err;
 
-  D(printf("Check connection... %d\n", i));
+  D(printf("Check connection... socket = %lx; h = %lx\n",
+              (long)(Socket_val(socket)), Handle_val(h)));
   if (WSAEnumNetworkEvents(Socket_val(socket), NULL, &evs)) {
-    win32_maperr(WSAGetLastError ());
-    uerror("WSAEnumNetworkEvents", Nothing);
+    caml_win32_maperr(WSAGetLastError());
+    caml_uerror("WSAEnumNetworkEvents", Nothing);
   }
   if (WSAEventSelect(Socket_val(socket), NULL, 0) == SOCKET_ERROR) {
-    win32_maperr(WSAGetLastError ());
-    uerror("WSAEventSelect", Nothing);
+    caml_win32_maperr(WSAGetLastError());
+    caml_uerror("WSAEventSelect", Nothing);
   }
-  if (!CloseHandle(events[i])) {
-    win32_maperr(GetLastError ());
-    uerror("CloseHandle", Nothing);
+  if (!CloseHandle(Handle_val(h))) {
+    caml_win32_maperr(GetLastError());
+    caml_uerror("CloseHandle", Nothing);
   }
   err =
     evs.iErrorCode[(Long_val(kind) == 0) ? FD_CONNECT_BIT : FD_ACCEPT_BIT];
   D(printf("Check connection: %ld, err %d\n", evs.lNetworkEvents, err));
   if (err != 0) {
-    win32_maperr(err);
-    uerror("check_connection", Nothing);
+    caml_win32_maperr(err);
+    caml_uerror("check_connection", Nothing);
   }
   CAMLreturn (Val_unit);
 }
@@ -423,7 +414,7 @@ CAMLprim value init_lwt (value callback) {
   int i;
 
   D(printf("Init...\n"));
-  register_global_root (&completionCallback);
+  caml_register_global_root(&completionCallback);
   completionCallback = callback;
 
   dummyEvent = CreateEvent(NULL, TRUE, FALSE, NULL);  // Dummy event
@@ -446,14 +437,16 @@ CAMLprim value init_lwt (value callback) {
   CAMLreturn (Val_long (MAXIMUM_WAIT_OBJECTS));
 }
 
-CAMLprim value win_wait (value timeout, value event_count) {
-  CAMLparam2(timeout, event_count);
+CAMLprim value win_wait (value timeout, value event_list) {
+  CAMLparam2(timeout, event_list);
   DWORD t, t2;
   DWORD res;
-  long ret, n = Long_val(event_count);
+  long ret, n;
   t = Long_val(timeout);
   if (t < 0) t = INFINITE;
   t2 = (compN > 0) ? 0 : t;
+  for (n = 0; event_list != Val_emptylist; event_list = Field(event_list, 1))
+    events[n++] = Handle_val(Field(event_list, 0));
   D(printf("Waiting: %ld events, timeout %ldms -> %ldms\n", n, t, t2));
   res =
     (n > 0) ?
@@ -473,8 +466,8 @@ CAMLprim value win_wait (value timeout, value event_count) {
   case WAIT_FAILED:
     D(printf("Wait failed\n"));
     ret = 0;
-    win32_maperr (GetLastError ());
-    uerror("WaitForMultipleObjectsEx", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("WaitForMultipleObjectsEx", Nothing);
     break;
   default:
     ret = res;
@@ -487,7 +480,7 @@ CAMLprim value win_wait (value timeout, value event_count) {
 
 static long pipeSerial;
 
-value win_pipe(long readMode, long writeMode) {
+value win_pipe(int cloexec, long readMode, long writeMode) {
   CAMLparam0();
   SECURITY_ATTRIBUTES attr;
   HANDLE readh, writeh;
@@ -496,7 +489,7 @@ value win_pipe(long readMode, long writeMode) {
 
   attr.nLength = sizeof(attr);
   attr.lpSecurityDescriptor = NULL;
-  attr.bInheritHandle = TRUE;
+  attr.bInheritHandle = cloexec ? FALSE : TRUE;
 
   sprintf(name, "\\\\.\\Pipe\\UnisonAnonPipe.%08lx.%08lx",
              GetCurrentProcessId(), pipeSerial++);
@@ -507,8 +500,8 @@ value win_pipe(long readMode, long writeMode) {
      1, UNIX_BUFFER_SIZE, UNIX_BUFFER_SIZE, 0, &attr);
 
   if (readh == INVALID_HANDLE_VALUE) {
-    win32_maperr(GetLastError());
-    uerror("CreateNamedPipe", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("CreateNamedPipe", Nothing);
     return FALSE;
   }
 
@@ -518,39 +511,39 @@ value win_pipe(long readMode, long writeMode) {
      FILE_ATTRIBUTE_NORMAL | writeMode, NULL);
 
   if (writeh == INVALID_HANDLE_VALUE) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     CloseHandle(readh);
-    uerror("CreateFile", Nothing);
+    caml_uerror("CreateFile", Nothing);
     return FALSE;
   }
 
-  readfd = win_alloc_handle(readh);
-  writefd = win_alloc_handle(writeh);
-  res = alloc_small(2, 0);
-  Store_field(res, 0, readfd);
-  Store_field(res, 1, writefd);
+  readfd = caml_win32_alloc_handle(readh);
+  writefd = caml_win32_alloc_handle(writeh);
+  res = caml_alloc_small(2, 0);
+  Field(res, 0) = readfd;
+  Field(res, 1) = writefd;
   CAMLreturn (res);
 }
 
-CAMLprim value win_pipe_in (value unit) {
+CAMLprim value win_pipe_in (value cloexec, value unit) {
   CAMLparam0();
-  CAMLreturn (win_pipe (FILE_FLAG_OVERLAPPED, 0));
+  CAMLreturn (win_pipe (caml_unix_cloexec_p(cloexec), FILE_FLAG_OVERLAPPED, 0));
 }
 
-CAMLprim value win_pipe_out (value unit) {
+CAMLprim value win_pipe_out (value cloexec, value unit) {
   CAMLparam0();
-  CAMLreturn (win_pipe (0, FILE_FLAG_OVERLAPPED));
+  CAMLreturn (win_pipe (caml_unix_cloexec_p(cloexec), 0, FILE_FLAG_OVERLAPPED));
 }
 
 static int socket_domain_table[] = {
-  PF_UNIX, PF_INET
+  PF_UNIX, PF_INET, PF_INET6
 };
 
 static int socket_type_table[] = {
   SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_SEQPACKET
 };
 
-CAMLprim value win_socket (value domain, value type, value proto) {
+CAMLprim value win_socket (value cloexec, value domain, value type, value proto) {
   CAMLparam3(domain, type, proto);
   SOCKET s;
 
@@ -560,10 +553,13 @@ CAMLprim value win_socket (value domain, value type, value proto) {
                 NULL, 0, WSA_FLAG_OVERLAPPED);
   D(printf("Created socket %lx\n", (long)s));
   if (s == INVALID_SOCKET) {
-    win32_maperr(WSAGetLastError ());
-    uerror("WSASocket", Nothing);
+    caml_win32_maperr(WSAGetLastError());
+    caml_uerror("WSASocket", Nothing);
   }
-  CAMLreturn(win_alloc_socket(s));
+  /* Ignore errors */
+  SetHandleInformation((HANDLE) s, HANDLE_FLAG_INHERIT,
+                       unix_cloexec_p(cloexec) ? 0 : HANDLE_FLAG_INHERIT);
+  CAMLreturn(caml_win32_alloc_socket(s));
 }
 
 /*
@@ -623,19 +619,18 @@ static int notify_filter_flags[8] = {
 CAMLprim value win_readdirtorychanges
 (value fd_val, value buf_val, value recursive, value flags, value id_val) {
   CAMLparam5(fd_val, buf_val, recursive, flags, id_val);
-  struct caml_bigarray *buf_arr = Bigarray_val(buf_val);
   long id = Long_val(id_val);
   HANDLE fd = Handle_val(fd_val);
-  char * buf = Array_data (buf_arr, 0);
-  long len = buf_arr->dim[0];
+  char * buf = Array_data(buf_val, 0);
+  long len = Caml_ba_array_val(buf_val)->dim[0];
   long action = READDIRECTORYCHANGES;
   BOOL res;
   long err;
-  int notify_filter = convert_flag_list(flags, notify_filter_flags);
+  int notify_filter = caml_convert_flag_list(flags, notify_filter_flags);
   completionData * d = GlobalAlloc(GPTR, sizeof(completionData));
   if (d == NULL) {
     errno = ENOMEM;
-    uerror(action_name[action], Nothing);
+    caml_uerror(action_name[action], Nothing);
   }
   d->id = id;
   d->action = action;
@@ -649,10 +644,10 @@ CAMLprim value win_readdirtorychanges
   if (!res) {
     err = GetLastError ();
     if (err != ERROR_IO_PENDING) {
-      win32_maperr (err);
+      caml_win32_maperr(err);
   D(printf("Action %s failed: id %ld -> err %d (errCode %ld)\n",
            action_name[action], id, errno, err));
-      uerror("ReadDirectoryChangesW", Nothing);
+      caml_uerror("ReadDirectoryChangesW", Nothing);
     }
   }
   CAMLreturn (Val_unit);
@@ -661,17 +656,18 @@ CAMLprim value win_readdirtorychanges
 CAMLprim value win_parse_directory_changes (value buf_val) {
   CAMLparam1(buf_val);
   CAMLlocal4(lst, tmp, elt, filename);
-  struct caml_bigarray *buf_arr = Bigarray_val(buf_val);
-  char * pos = Array_data (buf_arr, 0);
+  char * pos = Array_data(buf_val, 0);
   FILE_NOTIFY_INFORMATION * entry;
+  wchar_t *namebuf;
 
   lst = Val_long(0);
   while (1) {
     entry = (FILE_NOTIFY_INFORMATION *)pos;
+    namebuf = calloc(entry->FileNameLength + 2, 1);
+    memmove(namebuf, entry->FileName, entry->FileNameLength);
     elt = caml_alloc_tuple(2);
-    filename = caml_alloc_string(entry->FileNameLength);
-    memmove((char *)String_val(filename), entry->FileName, entry->FileNameLength);
-    Store_field (elt, 0, filename);
+    Store_field (elt, 0, caml_copy_string_of_utf16(namebuf));
+    free(namebuf);
     Store_field (elt, 1, Val_long(entry->Action - 1));
     tmp = caml_alloc_tuple(2);
     Store_field (tmp, 0, elt);
@@ -683,31 +679,34 @@ CAMLprim value win_parse_directory_changes (value buf_val) {
   CAMLreturn(lst);
 }
 
-CAMLprim value win_open_directory (value path, value wpath) {
-  CAMLparam2 (path, wpath);
+CAMLprim value win_open_directory (value path) {
+  CAMLparam1 (path);
   HANDLE h;
-  h = CreateFileW((LPCWSTR) String_val(wpath),
+  wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
+
+  h = CreateFileW(wpath,
                   FILE_LIST_DIRECTORY,
                   FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                   NULL,
                   OPEN_EXISTING,
                   FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
                   NULL);
+  caml_stat_free(wpath);
   if (h == INVALID_HANDLE_VALUE) {
-    win32_maperr (GetLastError ());
-    uerror("open", path);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("open", path);
   }
-  CAMLreturn(win_alloc_handle(h));
+  CAMLreturn(caml_win32_alloc_handle(h));
 }
 
-value copy_wstring(LPCWSTR s);
-
 CAMLprim value win_long_path_name(value path) {
   CAMLparam1(path);
+  wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
   wchar_t lbuf[32768] = L"";
   DWORD res;
 
-  res = GetLongPathNameW((LPCWSTR) String_val(path), lbuf, 32768);
+  res = GetLongPathNameW(wpath, lbuf, 32768);
+  caml_stat_free(wpath);
 
-  CAMLreturn(res == 0 || res > 32767 ? path : copy_wstring(lbuf));
+  CAMLreturn(res == 0 || res > 32767 ? path : caml_copy_string_of_utf16(lbuf));
 }
diff --git a/src/lwt/lwt_util.ml b/src/lwt/lwt_util.ml
index 3e563a5..cf9b4ef 100644
--- a/src/lwt/lwt_util.ml
+++ b/src/lwt/lwt_util.ml
@@ -61,6 +61,8 @@ let make_region count = { size = count; count = 0; waiters = Queue.create () }
 
 let resize_region reg sz = reg.size <- sz
 
+let purge_region reg = Queue.clear reg.waiters
+
 let leave_region reg sz =
    try
      if reg.count - sz >= reg.size then raise Queue.Empty;
diff --git a/src/lwt/lwt_util.mli b/src/lwt/lwt_util.mli
index 7dbc435..da0d38a 100644
--- a/src/lwt/lwt_util.mli
+++ b/src/lwt/lwt_util.mli
@@ -43,3 +43,8 @@ val run_in_region : region -> int -> (unit -> 'a Lwt.t) -> 'a Lwt.t
       (* [run_in_region reg size f] execute the thread produced by the
          function [f] in the region [reg]. The thread is not started
          before some room is available in the region. *)
+val purge_region : region -> unit
+      (* [purge_region reg] clear the queue of threads waiting to be
+         executed in the region [reg]. The waiting threads are not
+         woken (neither to execute nor to fail). Threads already being
+         executed in the region are not affected. *)
diff --git a/src/lwt/win/lwt_unix_impl.ml b/src/lwt/win/lwt_unix_impl.ml
index bccd2e2..fa0480f 100644
--- a/src/lwt/win/lwt_unix_impl.ml
+++ b/src/lwt/win/lwt_unix_impl.ml
@@ -84,19 +84,9 @@ external init_lwt :
 
 let max_event_count = init_lwt actionCompleted
 
-let event_count = ref 0
-let free_list = Array.init max_event_count (fun i -> i)
-
-let acquire_event nm =
-  if !event_count = max_event_count then
-    raise (Unix.Unix_error (Unix.EAGAIN, nm, ""));
-  let i = free_list.(!event_count) in
-  incr event_count;
-  i
-
-let release_event i =
-  decr event_count;
-  free_list.(!event_count) <- i
+let acquire_event l nm =
+  if List.length l = max_event_count then
+    raise (Unix.Unix_error (Unix.EAGAIN, nm, ""))
 
 (****)
 
@@ -158,7 +148,6 @@ module IntTbl =
     (struct type t = int let equal (x : int) y = x = y let hash x = x end)
 
 let ioInFlight = IntTbl.create 17
-let connInFlight = IntTbl.create 17
 
 let handleCompletionEvent (id, len, errno, name) =
 if !d then Format.eprintf "Handling event %d (len %d)@." id len;
@@ -178,17 +167,21 @@ if !d then Format.eprintf "Handling event %d (len %d)@." id len;
   else
     Lwt.wakeup res len
 
+type handle
+
+let connInFlight = ref []
+
 type kind = CONNECT | ACCEPT
 
-external win_wait : int -> int -> int = "win_wait"
+external win_wait : int -> handle list -> int = "win_wait"
 
 external win_register_wait :
-  Unix.file_descr -> kind -> int -> unit = "win_register_wait"
+  Unix.file_descr -> kind -> handle = "win_register_wait"
 
 external win_check_connection :
-  Unix.file_descr -> kind -> int -> unit = "win_check_connection"
+  Unix.file_descr -> kind -> handle -> unit = "win_check_connection"
 
-let handle_wait_event i ch kind cont action =
+let handle_wait_event h ch kind cont action =
 if !d then prerr_endline "MMM";
   let res =
     try
@@ -196,20 +189,19 @@ if !d then prerr_endline "MMM";
     with
       Unix.Unix_error ((Unix.EAGAIN | Unix.EWOULDBLOCK | Unix.EINTR), _, _) ->
 if !d then prerr_endline "NNN";
-        win_register_wait ch.fd kind i;
+        let h' = win_register_wait ch.fd kind in
+        connInFlight := List.map (fun el -> if fst el <> h then el else (h', snd el)) !connInFlight;
         None
     | e ->
 if !d then prerr_endline "OOO";
-        release_event i;
-        IntTbl.remove connInFlight i;
+        connInFlight := List.filter (fun (h', _) -> h' <> h) !connInFlight;
         Lwt.wakeup_exn cont e;
         None
   in
   match res with
     Some v ->
 if !d then prerr_endline "PPP";
-      release_event i;
-      IntTbl.remove connInFlight i;
+      connInFlight := List.filter (fun (h', _) -> h' <> h) !connInFlight;
       Lwt.wakeup cont v
   | None ->
       ()
@@ -237,7 +229,7 @@ if !d then Format.eprintf "DONE!@.";
 if !d then Format.eprintf "vvv@.";
       let i =
         try
-          win_wait (truncate (ceil (delay *. 1000.))) !event_count
+          win_wait (truncate (ceil (delay *. 1000.))) (List.map fst !connInFlight)
         with
           Sys.Break as e -> raise e
         | _              -> assert false
@@ -250,94 +242,21 @@ if !d then Format.eprintf "threads restarted@.";
       completionEvents := [];
       List.iter handleCompletionEvent (List.rev ev);
       if i >= 0 then begin
-        let (kind, ch) =
-          try IntTbl.find connInFlight i with Not_found -> assert false in
+        let (h, (kind, ch)) =
+          try List.nth !connInFlight i with Failure _ -> assert false in
         match kind with
           `CheckSocket res  ->
 if !d then prerr_endline "CHECK CONN";
-            handle_wait_event i ch CONNECT res
-               (fun () -> win_check_connection ch.fd CONNECT i)
+            handle_wait_event h ch CONNECT res
+               (fun () -> win_check_connection ch.fd CONNECT h)
         | `Accept res ->
 if !d then prerr_endline "ACCEPT";
-            handle_wait_event i ch ACCEPT res
+            handle_wait_event h ch ACCEPT res
               (fun () ->
-                 win_check_connection ch.fd ACCEPT i;
-                 let (v, info) = Unix.accept ch.fd in
+                 win_check_connection ch.fd ACCEPT h;
+                 let (v, info) = Unix.accept ~cloexec:true ch.fd in
                  (wrap_async v, info))
       end;
-(*
-      let infds = List.map fst !inputs in
-      let outfds = List.map fst !outputs in
-      let (readers, writers, _) =
-        if windows_hack && not recent_ocaml then
-          let writers = outfds in
-          let readers =
-            if delay = 0. || writers <> [] then [] else infds in
-          (readers, writers, [])
-        else if infds = [] && outfds = [] && delay = 0. then
-          ([], [], [])
-        else
-          try
-            let res = Unix.select infds outfds [] delay in
-            if delay > 0. && !now <> -1. then now := !now +. delay;
-            res
-          with
-            Unix.Unix_error (Unix.EINTR, _, _) ->
-              ([], [], [])
-          | Unix.Unix_error (Unix.EBADF, _, _) ->
-              (List.filter bad_fd infds, List.filter bad_fd outfds, [])
-          | Unix.Unix_error (Unix.EPIPE, _, _)
-            when windows_hack && recent_ocaml ->
-            (* Workaround for a bug in Ocaml 3.11: select fails with an
-               EPIPE error when the file descriptor is remotely closed *)
-              (infds, [], [])
-      in
-      restart_threads !event_counter now;
-      List.iter
-        (fun fd ->
-           try
-             match List.assoc fd !inputs with
-               `Read (buf, pos, len, res) ->
-                  wrap_syscall inputs fd res
-                    (fun () -> Unix.read fd buf pos len)
-             | `Accept res ->
-                  wrap_syscall inputs fd res
-                    (fun () ->
-                       let (s, i) = Unix.accept fd.fd in
-                       if not windows_hack then Unix.set_nonblock s;
-                       (wrap_async s, i))
-             | `Wait res ->
-                  wrap_syscall inputs fd res (fun () -> ())
-           with Not_found ->
-             ())
-        readers;
-      List.iter
-        (fun fd ->
-           try
-             match List.assoc fd !outputs with
-               `Write (buf, pos, len, res) ->
-                  wrap_syscall outputs fd res
-                    (fun () -> Unix.write fd buf pos len)
-             | `WriteSubstring (buf, pos, len, res) ->
-                  wrap_syscall outputs fd res
-                    (fun () -> Unix.write_substring fd buf pos len)
-             | `Wait res ->
-                  wrap_syscall inputs fd res (fun () -> ())
-           with Not_found ->
-             ())
-        writers;
-      if !child_exited then begin
-        child_exited := false;
-        List.iter
-          (fun (id, (res, flags, pid)) ->
-             wrap_syscall wait_children id res
-               (fun () ->
-                  let (pid', _) as v = Unix.waitpid flags pid in
-                  if pid' = 0 then raise Exit;
-                  v))
-          !wait_children
-      end;
-*)
       run thread
 
 (****)
@@ -387,23 +306,24 @@ if !d then Format.eprintf "Writing started@.";
   res
 
 external win_pipe_in :
-  unit -> Unix.file_descr * Unix.file_descr = "win_pipe_in"
+  ?cloexec:bool -> unit -> Unix.file_descr * Unix.file_descr = "win_pipe_in"
 external win_pipe_out :
-  unit -> Unix.file_descr * Unix.file_descr = "win_pipe_out"
+  ?cloexec:bool -> unit -> Unix.file_descr * Unix.file_descr = "win_pipe_out"
 
-let pipe_in () =
-  let (i, o) = if no_overlapped_io then Unix.pipe () else win_pipe_in () in
+let pipe_in ?cloexec () =
+  let (i, o) = if no_overlapped_io then Unix.pipe () else win_pipe_in ?cloexec () in
   (wrap_async i, o)
-let pipe_out () =
-  let (i, o) = if no_overlapped_io then Unix.pipe () else win_pipe_out () in
+let pipe_out ?cloexec () =
+  let (i, o) = if no_overlapped_io then Unix.pipe () else win_pipe_out ?cloexec () in
   (i, wrap_async o)
 
-external win_socket :
+external win_socket : ?cloexec:bool ->
   Unix.socket_domain -> Unix.socket_type -> int -> Unix.file_descr =
   "win_socket"
 
-let socket d t p =
-  let s = if no_overlapped_io then Unix.socket d t p else win_socket d t p in
+let socket ?cloexec d t p =
+  let s = if no_overlapped_io then Unix.socket ?cloexec d t p
+          else win_socket ?cloexec d t p in
   Unix.set_nonblock s;
   wrap_async s
 
@@ -418,16 +338,16 @@ let close ch = Unix.close ch.fd; kill_threads ch
 
 let accept ch =
   let res = Lwt.wait () in
-  let i = acquire_event "accept" in
-  IntTbl.add connInFlight i (`Accept res, ch);
-  win_register_wait ch.fd ACCEPT i;
+  let () = acquire_event !connInFlight "accept" in
+  let h = win_register_wait ch.fd ACCEPT in
+  connInFlight := (h, (`Accept res, ch)) :: !connInFlight;
   res
 
 let check_socket ch =
   let res = Lwt.wait () in
-  let i = acquire_event "connect" in
-  IntTbl.add connInFlight i (`CheckSocket res, ch);
-  win_register_wait ch.fd CONNECT i;
+  let () = acquire_event !connInFlight "connect" in
+  let h = win_register_wait ch.fd CONNECT in
+  connInFlight := (h, (`CheckSocket res, ch)) :: !connInFlight;
   res
 
 let connect s addr =
@@ -444,227 +364,6 @@ if !d then prerr_endline "BBB";
 if !d then prerr_endline "CCC";
       Lwt.fail e
 
-(*
-let ids = ref 0
-let new_id () = incr ids; !ids
-
-let _waitpid flags pid =
-  try
-    Lwt.return (Unix.waitpid flags pid)
-  with e ->
-    Lwt.fail e
-
-let waitpid flags pid =
-  if List.mem Unix.WNOHANG flags || windows_hack then
-    _waitpid flags pid
-  else
-    let flags = Unix.WNOHANG :: flags in
-    Lwt.bind (_waitpid flags pid) (fun ((pid', _) as res) ->
-    if pid' <> 0 then
-      Lwt.return res
-    else
-      let res = Lwt.wait () in
-      wait_children := (new_id (), (res, flags, pid)) :: !wait_children;
-      res)
-
-let wait () = waitpid [] (-1)
-
-let system cmd =
-  match Unix.fork () with
-     0 -> Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
-  | id -> Lwt.bind (waitpid [] id) (fun (pid, status) -> Lwt.return status)
-*)
-
-(****)
-(*
-type lwt_in_channel = in_channel
-type lwt_out_channel = out_channel
-
-let intern_in_channel ch =
-  Unix.set_nonblock (Unix.descr_of_in_channel ch); ch
-let intern_out_channel ch =
-  Unix.set_nonblock (Unix.descr_of_out_channel ch); ch
-
-
-let wait_inchan ic = wait_read (Unix.descr_of_in_channel ic)
-let wait_outchan oc = wait_write (Unix.descr_of_out_channel oc)
-
-let rec input_char ic =
-  try
-    Lwt.return (Pervasives.input_char ic)
-  with
-    Sys_blocked_io ->
-      Lwt.bind (wait_inchan ic) (fun () -> input_char ic)
-  | e ->
-      Lwt.fail e
-
-let rec input ic s ofs len =
-  try
-    Lwt.return (Pervasives.input ic s ofs len)
-  with
-    Sys_blocked_io ->
-      Lwt.bind (wait_inchan ic) (fun () -> input ic s ofs len)
-  | e ->
-      Lwt.fail e
-
-let rec unsafe_really_input ic s ofs len =
-  if len <= 0 then
-    Lwt.return ()
-  else begin
-    Lwt.bind (input ic s ofs len) (fun r ->
-    if r = 0
-    then Lwt.fail End_of_file
-    else unsafe_really_input ic s (ofs+r) (len-r))
-  end
-
-let really_input ic s ofs len =
-  if ofs < 0 || len < 0 || ofs > Bytes.length s - len
-  then Lwt.fail (Invalid_argument "really_input")
-  else unsafe_really_input ic s ofs len
-
-let input_line ic =
-  let buf = ref (Bytes.create 128) in
-  let pos = ref 0 in
-  let rec loop () =
-    if !pos = Bytes.length !buf then begin
-      let newbuf = Bytes.create (2 * !pos) in
-      Bytes.blit !buf 0 newbuf 0 !pos;
-      buf := newbuf
-    end;
-    Lwt.bind (input_char ic) (fun c ->
-    if c = '\n' then
-      Lwt.return ()
-    else begin
-      Bytes.set !buf !pos c;
-      incr pos;
-      loop ()
-    end)
-  in
-  Lwt.bind
-    (Lwt.catch loop
-       (fun e ->
-          match e with
-            End_of_file when !pos <> 0 ->
-              Lwt.return ()
-          | _ ->
-              Lwt.fail e))
-    (fun () ->
-       let res = Bytes.create !pos in
-       Bytes.blit !buf 0 res 0 !pos;
-       Lwt.return (Bytes.to_string res))
-*)
-(****)
-
-(*
-type popen_process =
-    Process of in_channel * out_channel
-  | Process_in of in_channel
-  | Process_out of out_channel
-  | Process_full of in_channel * out_channel * in_channel
-
-let popen_processes = (Hashtbl.create 7 : (popen_process, int) Hashtbl.t)
-
-let open_proc cmd proc input output toclose =
-  match Unix.fork () with
-     0 -> if input <> Unix.stdin then begin
-            Unix.dup2 input Unix.stdin;
-            Unix.close input
-          end;
-          if output <> Unix.stdout then begin
-            Unix.dup2 output Unix.stdout;
-            Unix.close output
-          end;
-          List.iter Unix.close toclose;
-          Unix.execv "/bin/sh" [| "/bin/sh"; "-c"; cmd |]
-  | id -> Hashtbl.add popen_processes proc id
-
-let open_process_in cmd =
-  let (in_read, in_write) = pipe_in () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  open_proc cmd (Process_in inchan) Unix.stdin in_write [in_read];
-  Unix.close in_write;
-  Lwt.return inchan
-
-let open_process_out cmd =
-  let (out_read, out_write) = pipe_out () in
-  let outchan = Unix.out_channel_of_descr out_write in
-  open_proc cmd (Process_out outchan) out_read Unix.stdout [out_write];
-  Unix.close out_read;
-  Lwt.return outchan
-
-let open_process cmd =
-  let (in_read, in_write) = pipe_in () in
-  let (out_read, out_write) = pipe_out () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  let outchan = Unix.out_channel_of_descr out_write in
-  open_proc cmd (Process(inchan, outchan)) out_read in_write
-                                           [in_read; out_write];
-  Unix.close out_read;
-  Unix.close in_write;
-  Lwt.return (inchan, outchan)
-
-(* FIX: Subprocesses that use /dev/tty to print things on the terminal
-   will NOT have this output captured and returned to the caller of this
-   function.  There's an argument that this is correct, but if we are
-   running from a GUI the user may not be looking at any terminal and it
-   will appear that the process is just hanging.  This can be fixed, in
-   principle, by writing a little C code that opens /dev/tty and then uses
-   the TIOCNOTTY ioctl control to detach the terminal. *)
-
-let open_proc_full cmd env proc input output error toclose =
-  match Unix.fork () with
-     0 -> Unix.dup2 input Unix.stdin; Unix.close input;
-          Unix.dup2 output Unix.stdout; Unix.close output;
-          Unix.dup2 error Unix.stderr; Unix.close error;
-          List.iter Unix.close toclose;
-          Unix.execve "/bin/sh" [| "/bin/sh"; "-c"; cmd |] env
-  | id -> Hashtbl.add popen_processes proc id
-
-let open_process_full cmd env =
-  let (in_read, in_write) = pipe_in () in
-  let (out_read, out_write) = pipe_out () in
-  let (err_read, err_write) = pipe_in () in
-  let inchan = Unix.in_channel_of_descr in_read in
-  let outchan = Unix.out_channel_of_descr out_write in
-  let errchan = Unix.in_channel_of_descr err_read in
-  open_proc_full cmd env (Process_full(inchan, outchan, errchan))
-                 out_read in_write err_write [in_write; out_read; err_read];
-  Unix.close out_read;
-  Unix.close in_write;
-  Unix.close err_write;
-  Lwt.return (inchan, outchan, errchan)
-
-let find_proc_id fun_name proc =
-  try
-    let pid = Hashtbl.find popen_processes proc in
-    Hashtbl.remove popen_processes proc;
-    pid
-  with Not_found ->
-    raise (Unix.Unix_error (Unix.EBADF, fun_name, ""))
-*)
-(*
-let close_process_in inchan =
-  let pid = find_proc_id "close_process_in" (Process_in inchan) in
-  close_in inchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process_out outchan =
-  let pid = find_proc_id "close_process_out" (Process_out outchan) in
-  close_out outchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process (inchan, outchan) =
-  let pid = find_proc_id "close_process" (Process(inchan, outchan)) in
-  close_in inchan; close_out outchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-
-let close_process_full (outchan, inchan, errchan) =
-  let pid =
-    find_proc_id "close_process_full"
-                 (Process_full(outchan, inchan, errchan)) in
-  close_out inchan; close_in outchan; close_in errchan;
-  Lwt.bind (waitpid [] pid) (fun (_, status) -> Lwt.return status)
-*)
 
 type lwt_in_channel
 let input_line _ = assert false (*XXXXX*)
@@ -674,8 +373,8 @@ let intern_in_channel _ = assert false (*XXXXX*)
 
 type directory_handle = Unix.file_descr
 
-external open_dir : string -> string -> directory_handle = "win_open_directory"
-let open_directory f = open_dir f (System_impl.Fs.W.epath f)
+external open_dir : string -> directory_handle = "win_open_directory"
+let open_directory f = open_dir (System_win.extendedPath f)
 
 type notify_filter_flag =
     FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME
@@ -707,9 +406,7 @@ if !d then Format.eprintf "Reading started@.";
   if len = 0 then
     Lwt.return []
   else
-    Lwt.return (List.rev_map (fun (nm, act) ->
-                                (System_impl.Fs.W.path8 nm, act))
-                        (parse_directory_changes buf)))
+    Lwt.return (List.rev (parse_directory_changes buf)))
 
 let close_dir = Unix.close
 
@@ -718,12 +415,10 @@ external long_name : string -> string = "win_long_path_name"
 let longpathname root path =
   (* Parameter [path] can be relative. Result value must then also be relative.
      Input parameter to [long_name] must always be absolute path. *)
-  let epath = System_impl.Fs.W.epath (Filename.concat root path)
-  and root = System_impl.Fs.W.epath (Filename.concat root "") in
-  let root = String.sub root 0 (String.length root - 2) in (* Remove trailing \000\000 *)
+  let epath = System_win.extendedPath (Filename.concat root path)
+  and root = System_win.extendedPath (Filename.concat root "") in
   let start = String.length root
   and ln = long_name epath in
-  let n =
   try
     (* The assumption is that [root] does not change in [long_name]. The
        Windows fsmonitor operates under this assumption, so it is ok here.
@@ -732,4 +427,3 @@ let longpathname root path =
     String.sub ln start (String.length ln - start)
   with
   | Invalid_argument _ -> ln
-  in System_impl.Fs.W.path8 n
diff --git a/src/main.ml b/src/main.ml
index 411bd2e..c48c769 100644
--- a/src/main.ml
+++ b/src/main.ml
@@ -57,13 +57,18 @@
 
 let versionPrefName = "version"
 let printVersionAndExit =
-  Prefs.createBool versionPrefName false "print version and exit"
+  Prefs.createBool versionPrefName false
+    ~category:(`Basic `General)
+    ~cli_only:true
+    "print version and exit"
     ("Print the current version number and exit.  "
      ^ "(This option only makes sense on the command line.)")
 
 let docsPrefName = "doc"
 let docs =
   Prefs.createString docsPrefName ""
+    ~category:(`Basic `General)
+    ~cli_only:true
     "show documentation ('-doc topics' lists topics)"
     (  "The command-line argument \\texttt{-doc \\ARG{secname}} causes unison to "
        ^ "display section  \\ARG{secname} of the manual on the standard output "
@@ -76,34 +81,57 @@ let docs =
 let prefsdocsPrefName = "prefsdocs"
 let prefsdocs =
   Prefs.createBool prefsdocsPrefName false
+    ~category:(`Internal `Devel)
+    ~cli_only:true
     "*show full documentation for all preferences (and then exit)"
     ""
 
+let prefsmanPrefName = "prefsman"
+let prefsman =
+  Prefs.createString prefsmanPrefName ""
+    ~category:(`Internal `Devel)
+    ~cli_only:true
+    "*show manpage documentation for all preferences (and then exit)"
+    ""
+
 let serverPrefName = "server"
 let server =
-  Prefs.createBool serverPrefName false "*normal or server mode" ""
+  Prefs.createBool serverPrefName false
+    ~category:(`Internal `Other)
+    ~cli_only:true
+    "*normal or server mode" ""
 
 let socketPrefName = "socket"
 let socket =
-  Prefs.create socketPrefName None
-    "!act as a server on a socket" ""
-    (fun _ -> fun i ->
-      (try
-         Some(int_of_string i)
-       with Failure _ ->
-         raise(Prefs.IllegalValue "-socket must be followed by a number")))
-    (function None -> [] | Some(i) -> [string_of_int i]) ;;
-
-let serverHostName = "host"
+  Prefs.createString socketPrefName ""
+    ~category:(`Advanced `Remote)
+    ~cli_only:true
+    "act as a server on a socket"
+    ("Start " ^ Uutil.myName ^ " as a server listening on a TCP socket "
+     ^ "(with TCP port number as argument) or a local socket (aka Unix "
+     ^ "domain socket) (with socket path as argument).")
+
+let serverHostNameAlias = "host"
+let serverHostName = "listen"
 let serverHost =
   Prefs.createString serverHostName ""
-    "!bind the socket to this host name in server socket mode" ""
+    ~category:(`Advanced `Remote)
+    ~cli_only:true
+    "listen on this name or addr in server socket mode (can repeat)"
+    ("When acting as a server on a TCP socket, Unison will by default listen "
+     ^ "on \"any\" address (0.0.0.0 and [::]).  This command-line argument "
+     ^ "allows to specify a different listening address and can be repeated "
+     ^ "to listen on multiple addresses.  Listening address can be specified "
+     ^ "as a host name or an IP address.")
+let () = Prefs.alias serverHost serverHostNameAlias
 
 (* User preference for which UI to use if there is a choice *)
 let uiPrefName = "ui"
 let interface =
   Prefs.create uiPrefName Uicommon.Graphic
-    "!select UI ('text' or 'graphic'); command-line only"
+    ~category:(`Advanced `General)
+    ~cli_only:true
+    "select UI ('text' or 'graphic'); command-line only"
     ("This preference selects either the graphical or the textual user "
      ^ "interface.  Legal values are \\verb|graphic| or \\verb|text|.  "
      ^ "\n\nBecause this option is processed specially during Unison's "
@@ -123,7 +151,8 @@ let interface =
                                       graphic -> graphic user interface\n"
                                       ^other^ " is not a legal value")))
     (function Uicommon.Text -> ["text"]
-      | Uicommon.Graphic -> ["graphic"]);;
+      | Uicommon.Graphic -> ["graphic"])
+    Uicommon.minterface
 
 let catch_all f =
   try
@@ -135,6 +164,13 @@ let catch_all f =
   with e ->
     Util.msg "Unison server failed: %s\n" (Uicommon.exn2string e); exit 1;;
 
+let gui_safe_printf fmt =
+  Printf.ksprintf (fun s ->
+    if System.has_stdout ~info:s then Printf.printf "%s" s) fmt
+
+let verify_stdout () =
+  if not (System.has_stdout ~info:"") then exit 37
+
 let init () = begin
   ignore (Gc.set {(Gc.get ()) with Gc.max_overhead = 150});
   (* Make sure exception descriptions include backtraces *)
@@ -144,20 +180,31 @@ let init () = begin
 
   (* Print version if requested *)
   if Util.StringMap.mem versionPrefName argv then begin
-    Printf.printf "%s version %s\n" Uutil.myName Uutil.myVersion;
+    gui_safe_printf "%s version %s\n" Uutil.myName Uutil.myVersion;
     exit 0
   end;
 
   (* Print docs for all preferences if requested (this is used when building
      the manual) *)
   if Util.StringMap.mem prefsdocsPrefName argv then begin
-    Prefs.printFullDocs();
+    Prefs.printFullDocs `TeX;
+    exit 0
+  end;
+
+  if Util.StringMap.mem prefsmanPrefName argv then begin
+    begin match Util.StringMap.find prefsmanPrefName argv with
+      | "short" :: _ -> Prefs.printUsageForMan ()
+      | "full" :: _ -> Prefs.printFullDocs `man
+      | _ -> ()
+    end;
     exit 0
   end;
 
   (* Display documentation if requested *)
   begin try
-    begin match Util.StringMap.find docsPrefName argv with
+    let docv = Util.StringMap.find docsPrefName argv in
+    verify_stdout ();
+    begin match docv with
       [] ->
         assert false
     | "topics"::_ ->
@@ -205,13 +252,8 @@ let init () = begin
     catch_all (fun () ->
      Os.createUnisonDir();
       Remote.waitOnPort
-        (begin try
-           match Util.StringMap.find serverHostName argv with
-             []     -> None
-           | s :: _ -> Some s
-         with Not_found ->
-           None
-         end)
+        ((try Util.StringMap.find serverHostName argv with Not_found -> []) @
+         (try Util.StringMap.find serverHostNameAlias argv with Not_found -> []))
         i);
     exit 0
   with Not_found -> () end;
@@ -237,8 +279,8 @@ module Body = functor (Ui : Uicommon.UI) -> struct
   Ui.start
     (try
       (match Util.StringMap.find uiPrefName argv with
-        "text"::_    -> Uicommon.Text
+      | "text"::_    -> verify_stdout (); Uicommon.Text
       | "graphic"::_ -> Uicommon.Graphic
-      | _            -> Prefs.printUsage Uicommon.usageMsg; exit 1)
+      | _ -> verify_stdout (); Prefs.printUsage Uicommon.usageMsg; exit 1)
     with Not_found -> Ui.defaultUi)
 end
diff --git a/src/name.ml b/src/name.ml
index b816ccd..f0c6116 100644
--- a/src/name.ml
+++ b/src/name.ml
@@ -20,6 +20,8 @@
    INCREMENT "UPDATE.ARCHIVEFORMAT" *)
 type t = string
 
+let m = Umarshal.string
+
 let compare n1 n2 = (Case.ops())#compare n1 n2
 
 let eq a b = (0 = (compare a b))
diff --git a/src/name.mli b/src/name.mli
index d2cb26c..a4fe896 100644
--- a/src/name.mli
+++ b/src/name.mli
@@ -3,6 +3,8 @@
 
 type t
 
+val m : t Umarshal.t
+
 val fromString : string -> t
 val toString : t -> string
 
diff --git a/src/negotiate.ml b/src/negotiate.ml
new file mode 100644
index 0000000..471343f
--- /dev/null
+++ b/src/negotiate.ml
@@ -0,0 +1,84 @@
+(* Unison file synchronizer: src/negotiate.ml *)
+(* Copyright 2021, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*)
+
+let (>>=) = Lwt.bind
+
+let debug = Util.debug "features"
+
+let debugFeatures name features =
+  debug (fun () ->
+    Util.msg "%s:\n" name;
+    Safelist.iter (fun n -> Util.msg "  - %s\n" n) features)
+
+let getCommonFeaturesLocal (root, features) =
+  Features.resetEnabled ();
+  let supportedFeatures = Features.all () in
+  debugFeatures "Supported features" supportedFeatures;
+  debugFeatures "Received features for feature negotiation" features;
+  let common = Features.inter features supportedFeatures in
+  debugFeatures "Selected common features" common;
+  try
+    let () = Features.validate common in
+    let () = Features.setEnabled common in
+    Lwt.return common
+  with
+  | e -> Lwt.fail e
+
+let m = Umarshal.(list string)
+
+let negotiateFeaturesRpcName = "negotiateFeatures"
+let getCommonFeaturesRemote =
+  Remote.registerRootCmd negotiateFeaturesRpcName m m getCommonFeaturesLocal
+
+let getCommonFeaturesOnRoot features = function
+  | (Common.Local, _) -> Lwt.return features
+  | root -> getCommonFeaturesRemote root features
+
+let commonFeatures root fts =
+  getCommonFeaturesOnRoot fts root >>= fun common ->
+  let rn = "Common features for root " ^ Common.root2string root in
+  debugFeatures rn common;
+  try
+    let () = Features.validate common in
+    Lwt.return common
+  with
+  | e -> Lwt.fail e
+
+let allRootsSupportFeatures roots =
+  let aux k r =
+    let supp = Remote.commandAvailable r negotiateFeaturesRpcName in
+    k >>= fun k' ->
+    supp >>= fun supp' ->
+    Lwt.return (k' && supp')
+  in
+  Safelist.fold_left aux (Lwt.return true) roots
+
+let features roots =
+  Features.resetEnabled ();
+  let supportedFeatures = Features.all () in
+  debugFeatures "Supported features" supportedFeatures;
+  allRootsSupportFeatures roots >>= (fun supported ->
+  if not supported then begin
+    debug (fun () -> Util.msg "The server does not support \"features\".\n");
+    Lwt.return (Features.empty)
+  end else
+    Safelist.fold_left (fun fts r -> fts >>= commonFeatures r)
+      (Lwt.return supportedFeatures) roots
+  ) >>= fun common ->
+  debugFeatures "Enabled features" common;
+  Lwt.return (Features.setEnabled common)
+
diff --git a/src/linkgtk.ml b/src/negotiate.mli
similarity index 83%
rename from src/linkgtk.ml
rename to src/negotiate.mli
index a777593..ae5b96a 100644
--- a/src/linkgtk.ml
+++ b/src/negotiate.mli
@@ -1,5 +1,5 @@
-(* Unison file synchronizer: src/linkgtk.ml *)
-(* Copyright 1999-2020, Benjamin C. Pierce
+(* Unison file synchronizer: src/negotiate.mli *)
+(* Copyright 2021, Tõivo Leedjärv
 
     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
@@ -15,5 +15,5 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
+val features : Common.root list -> unit Lwt.t
 
-module TopLevel = Main.Body(Uigtk.Body)
diff --git a/src/os.ml b/src/os.ml
index 89895dc..48fbb11 100644
--- a/src/os.ml
+++ b/src/os.ml
@@ -30,8 +30,9 @@ let localCanonicalHostName =
 
 let clientHostName : string Prefs.t =
   Prefs.createString "clientHostName" localCanonicalHostName
-    "!set host name of client"
-    ("When specified, the host name of the client will not be guessed" ^
+    ~category:(`Advanced `Remote)
+    "set host name of client"
+    ("When specified, the host name of the client will not be guessed " ^
      "and the provided host name will be used to find the archive.")
 
 let serverHostName = localCanonicalHostName
@@ -59,7 +60,7 @@ let isTempFile file =
 (*****************************************************************************)
 
 let exists fspath path =
-  (Fileinfo.get false fspath path).Fileinfo.typ <> `ABSENT
+  Fileinfo.getType false fspath path <> `ABSENT
 
 let readLink fspath path =
   Util.convertUnixErrorsToTransient
@@ -136,7 +137,7 @@ let rec childrenOf fspath path =
        else if isTempFile file then begin
          if Util.endswith file !tempFileSuffix then begin
            let p = Path.child path filename in
-           let i = Fileinfo.get false fspath p in
+           let i = Fileinfo.getBasic false fspath p in
            let secondsinthirtydays = 2592000.0 in
            if Props.time i.Fileinfo.desc +. secondsinthirtydays < Util.time()
            then begin
@@ -163,7 +164,7 @@ and delete fspath path =
     "deleting"
     (fun () ->
       let absolutePath = Fspath.concat fspath path in
-      match (Fileinfo.get false fspath path).Fileinfo.typ with
+      match Fileinfo.getType false fspath path with
         `DIRECTORY ->
           begin try
             Fs.chmod absolutePath 0o700
@@ -190,8 +191,10 @@ and delete fspath path =
       | `ABSENT ->
           ())
 
-let renameFspath fname source target =
+let rename fname sourcefspath sourcepath targetfspath targetpath =
+  let source = Fspath.concat sourcefspath sourcepath in
   let source' = Fspath.toPrintString source in
+  let target = Fspath.concat targetfspath targetpath in
   let target' = Fspath.toPrintString target in
   if source = target then
     raise (Util.Transient ("Rename ("^fname^"): identical source and target " ^ source'));
@@ -208,11 +211,6 @@ let renameFspath fname source target =
           Fs.unlink targetDouble
       end)
 
-let rename fname sourcefspath sourcepath targetfspath targetpath =
-  let source = Fspath.concat sourcefspath sourcepath in
-  let target = Fspath.concat targetfspath targetpath in
-  renameFspath fname source target
-
 let symlink =
   if Fs.hasSymlink () then
     fun fspath path l ->
@@ -234,12 +232,12 @@ let symlink =
                ))
 
 (* Create a new directory, using the permissions from the given props        *)
-let createDir fspath path props =
+let createDir fspath path perms =
   Util.convertUnixErrorsToTransient
   "creating directory"
     (fun () ->
        let absolutePath = Fspath.concat fspath path in
-       Fs.mkdir absolutePath (Props.perms props))
+       Fs.mkdir absolutePath perms)
 
 (*****************************************************************************)
 (*                              FINGERPRINTS                                 *)
@@ -247,9 +245,11 @@ let createDir fspath path props =
 
 type fullfingerprint = Fingerprint.t * Fingerprint.t
 
-let fingerprint fspath path info =
+let mfullfingerprint = Umarshal.(prod2 Fingerprint.m Fingerprint.m id id)
+
+let fingerprint fspath path typ =
   (Fingerprint.file fspath path,
-   Osx.ressFingerprint fspath path info.Fileinfo.osX)
+   Osx.ressFingerprint fspath path typ)
 
 let pseudoFingerprint path size =
   (Fingerprint.pseudo path size, Fingerprint.dummy)
@@ -275,7 +275,7 @@ let safeFingerprint fspath path info optFp =
         in
         let ressFp =
           match optRessFp with
-            None      -> Osx.ressFingerprint fspath path info.Fileinfo.osX
+            None      -> Osx.ressFingerprint fspath path info.Fileinfo.typ
           | Some ress -> ress
         in
         let (info', dataUnchanged, ressUnchanged) =
@@ -318,7 +318,7 @@ let createUnisonDir() =
   with Unix.Unix_error(_) ->
     Util.convertUnixErrorsToFatal
       (Printf.sprintf "creating unison directory %s"
-         (System.fspathToPrintString Util.unisonDir))
+         Util.unisonDir)
       (fun () ->
          ignore (System.mkdir Util.unisonDir 0o700))
 
diff --git a/src/os.mli b/src/os.mli
index 8c08804..48b17a8 100644
--- a/src/os.mli
+++ b/src/os.mli
@@ -16,9 +16,8 @@ val childrenOf : Fspath.t -> Path.local -> Name.t list
 val readLink : Fspath.t -> Path.local -> string
 val symlink : Fspath.t -> Path.local -> string -> unit
 
-val renameFspath : string -> Fspath.t -> Fspath.t -> unit
 val rename : string -> Fspath.t -> Path.local -> Fspath.t -> Path.local -> unit
-val createDir : Fspath.t -> Path.local -> Props.t -> unit
+val createDir : Fspath.t -> Path.local -> int -> unit
 val delete : Fspath.t -> Path.local -> unit
 
 (* We define a new type of fingerprints here so that clients of
@@ -28,6 +27,7 @@ val delete : Fspath.t -> Path.local -> unit
    is a pair of the main file's fingerprint and the resource fork fingerprint,
    if any. *)
 type fullfingerprint
+val mfullfingerprint : fullfingerprint Umarshal.t
 val fullfingerprint_to_string : fullfingerprint -> string
 val reasonForFingerprintMismatch : fullfingerprint -> fullfingerprint -> string
 val fullfingerprint_dummy : fullfingerprint
@@ -43,7 +43,7 @@ val safeFingerprint :
                             (* current fileinfo, fingerprint and fork info *)
 val fingerprint :
   Fspath.t -> Path.local -> (* coordinates of file to fingerprint *)
-  Fileinfo.t ->             (* old fileinfo *)
+  Fileinfo.typ ->           (* old fileinfo *)
   fullfingerprint           (* current fingerprint *)
 
 val pseudoFingerprint :
diff --git a/src/osx.ml b/src/osx.ml
index 4364d2a..a3d1c7f 100644
--- a/src/osx.ml
+++ b/src/osx.ml
@@ -32,7 +32,8 @@ let isMacOSX = isMacOSXPred ()
 
 let rsrcSync =
   Prefs.createBoolWithDefault "rsrc"
-    "!synchronize resource forks (true/false/default)"
+    ~category:(`Advanced `Sync)
+    "synchronize resource forks (true/false/default)"
     "When set to {\\tt true}, this flag causes Unison to synchronize \
      resource forks and HFS meta-data.  On filesystems that do not \
      natively support resource forks, this data is stored in \
@@ -46,6 +47,7 @@ let rsrcSync =
    to the other host during initialization *)
 let rsrc =
   Prefs.createBool "rsrc-aux" false
+    ~category:(`Internal `Pseudo)
     "*synchronize resource forks and HFS meta-data" ""
 
 let init b =
@@ -179,8 +181,21 @@ type 'a ressInfo =
   | HfsRess of Uutil.Filesize.t
   | AppleDoubleRess of int * float * float * Uutil.Filesize.t * 'a
 
+let mressInfo m = Umarshal.(sum3 unit Uutil.Filesize.m
+                              (prod5 int float float Uutil.Filesize.m m id id)
+                              (function
+                               | NoRess -> I31 ()
+                               | HfsRess a -> I32 a
+                               | AppleDoubleRess (a, b, c, d, e) -> I33 (a, b, c, d, e))
+                              (function
+                               | I31 () -> NoRess
+                               | I32 a -> HfsRess a
+                               | I33 (a, b, c, d, e) -> AppleDoubleRess (a, b, c, d, e)))
+
 type ressStamp = unit ressInfo
 
+let mressStamp = mressInfo Umarshal.unit
+
 let ressStampToString r =
   match r with
     NoRess         ->
@@ -195,10 +210,14 @@ type info =
   { ressInfo : (Fspath.t * int64) ressInfo;
     finfo : string }
 
+let minfo = Umarshal.(prod2 (mressInfo (prod2 Fspath.m int64 id id)) string
+                        (fun {ressInfo; finfo} -> ressInfo, finfo)
+                        (fun (ressInfo, finfo) -> {ressInfo; finfo}))
+
 external getFileInfosInternal :
-  System.fspath -> bool -> string * int64 = "getFileInfos"
+  string -> bool -> string * int64 = "getFileInfos"
 external setFileInfosInternal :
-  System.fspath -> string -> unit = "setFileInfos"
+  string -> string -> unit = "setFileInfos"
 
 let defaultInfos typ =
   match typ with
@@ -244,7 +263,7 @@ let getFileInfos dataFspath dataPath typ =
         try
           let (fInfo, rsrcLength) =
             getFileInfosInternal
-              (Fspath.toSysPath (Fspath.concat dataFspath dataPath))
+              (Fspath.toString (Fspath.concat dataFspath dataPath))
               (typ = `FILE)
           in
           { ressInfo =
@@ -255,7 +274,8 @@ let getFileInfos dataFspath dataPath typ =
           (* Not a HFS volume.  Look for an AppleDouble file *)
           try
             let (workingDir, realPath) =
-              Fspath.findWorkingDir dataFspath dataPath in
+              try Fspath.findWorkingDir dataFspath dataPath with
+              | Util.Transient _ -> raise Not_found in
             let (doubleFspath, inch, entries) =
               openDouble workingDir realPath in
             let (rsrcOffset, rsrcLength) =
@@ -342,18 +362,21 @@ let setFileInfos dataFspath dataPath finfo =
   assert (finfo <> "");
   Util.convertUnixErrorsToTransient "setting file information" (fun () ->
     try
-      let p = Fspath.toSysPath (Fspath.concat dataFspath dataPath) in
+      let p = Fspath.toString (Fspath.concat dataFspath dataPath) in
       let (fullFinfo, _) = getFileInfosInternal p false in
       setFileInfosInternal p (insertInfo (Bytes.of_string fullFinfo) finfo)
-    with Unix.Unix_error ((Unix.EOPNOTSUPP | Unix.ENOSYS), _, _) ->
+    with Unix.Unix_error ((EOPNOTSUPP | ENOSYS | EUNKNOWNERR 93), _, _) ->
+      (* ENOATTR (93) is returned on msdos/exfat fs since macOS 13 *)
       (* Not an HFS volume.  Look for an AppleDouble file *)
       let (workingDir, realPath) = Fspath.findWorkingDir dataFspath dataPath in
       begin try
         let (doubleFspath, inch, entries) = openDouble workingDir realPath in
         begin try
           let (ofs, len) = Safelist.assoc `FINFO entries in
-          if len < finfoLength then
-            fail dataFspath dataPath doubleFspath "bad finder info";
+          if len < finfoLength then begin
+            close_in_noerr inch;
+            fail dataFspath dataPath doubleFspath "bad finder info"
+          end;
           let fullFinfo =
             protect
               (fun () ->
@@ -452,7 +475,12 @@ let stamp info =
   | AppleDoubleRess (inode, mtime, ctime, len, _) ->
       AppleDoubleRess (inode, mtime, ctime, len, ())
 
-let ressFingerprint fspath path info =
+let ressFingerprint fspath path typ =
+  (* This function used to get ready-made info passed in. (Re-)getting the
+     info here may consume one or a few additional syscalls. This is not
+     thought to be a problem unless there are hundreds of thousands of files
+     with resource forks. That is really unlikely. *)
+  let info = getFileInfos fspath path typ in
   match info.ressInfo with
     NoRess ->
       Fingerprint.dummy
@@ -481,7 +509,7 @@ let openRessIn fspath path =
       Unix.in_channel_of_descr
         (Fs.openfile
            (Fspath.concat fspath (ressPath path))
-           [Unix.O_RDONLY] 0o444)
+           [Unix.O_RDONLY; O_CLOEXEC] 0o444)
     with Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
       let (doublePath, inch, entries) = openDouble fspath path in
       try
@@ -499,7 +527,7 @@ let openRessOut fspath path length =
       let p = Fspath.concat fspath (ressPath path) in
       debug (fun () -> Util.msg "openRessOut %s\n" (Fspath.toString p));
       Unix.out_channel_of_descr
-        (Fs.openfile p [Unix.O_WRONLY;Unix.O_CREAT] 0o600)
+        (Fs.openfile p [Unix.O_WRONLY; O_CREAT; O_CLOEXEC] 0o600)
     with Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
       debug (fun () -> Util.msg "Opening AppleDouble file for resource fork\n");
       let path = Fspath.appleDouble (Fspath.concat fspath path) in
diff --git a/src/osx.mli b/src/osx.mli
index c7a67f8..55c6c8d 100644
--- a/src/osx.mli
+++ b/src/osx.mli
@@ -12,6 +12,9 @@ type info =
   { ressInfo : (Fspath.t * int64) ressInfo;
     finfo : string }
 
+val mressStamp : ressStamp Umarshal.t
+val minfo : info Umarshal.t
+
 val defaultInfos :  [> `DIRECTORY | `FILE ] -> info
 
 val getFileInfos : Fspath.t -> Path.local -> [> `DIRECTORY | `FILE ] -> info
@@ -20,7 +23,7 @@ val setFileInfos : Fspath.t -> Path.local -> string -> unit
 val ressUnchanged :
   'a ressInfo -> 'b ressInfo -> float option -> bool -> bool
 
-val ressFingerprint : Fspath.t -> Path.local -> info -> Fingerprint.t
+val ressFingerprint : Fspath.t -> Path.local -> [> `DIRECTORY | `FILE ] -> Fingerprint.t
 val ressLength : 'a ressInfo -> Uutil.Filesize.t
 
 val ressDummy : ressStamp
diff --git a/src/osxsupport.c b/src/osxsupport.c
index 6fe0267..7de66f7 100644
--- a/src/osxsupport.c
+++ b/src/osxsupport.c
@@ -4,6 +4,7 @@
 #include <caml/mlvalues.h>
 #include <caml/alloc.h>
 #include <caml/memory.h>
+#include <caml/unixsupport.h>
 #ifdef __APPLE__
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -15,21 +16,25 @@
 #endif
 #include <errno.h>
 
-extern void unix_error (int errcode, char * cmdname, value arg) Noreturn;
-extern void uerror (char * cmdname, value arg) Noreturn;
+#include <caml/version.h>
+#if OCAML_VERSION_MAJOR < 5
+#define caml_unix_error unix_error
+#define caml_uerror uerror
+#endif
 
 CAMLprim value isMacOSX (value nothing) {
+  CAMLparam0();
 #ifdef __APPLE__
-  return Val_true;
+  CAMLreturn(Val_true);
 #else
-  return Val_false;
+  CAMLreturn(Val_false);
 #endif
 }
 
 CAMLprim value getFileInfos (value path, value need_size) {
 #ifdef __APPLE__
 
-  CAMLparam1(path);
+  CAMLparam2(path, need_size);
   CAMLlocal3(res, fInfo, length);
   int retcode;
   struct attrlist attrList;
@@ -54,24 +59,24 @@ CAMLprim value getFileInfos (value path, value need_size) {
   retcode = getattrlist(String_val (path), &attrList, &attrBuf,
                         sizeof attrBuf, options);
 
-  if (retcode == -1) uerror("getattrlist", path);
+  if (retcode == -1) caml_uerror("getattrlist", path);
 
   if (Bool_val (need_size)) {
     if (attrBuf.length != sizeof attrBuf)
-      unix_error (EINVAL, "getattrlist", path);
+      caml_unix_error(EINVAL, "getattrlist", path);
   } else {
     if (attrBuf.length != sizeof (u_int32_t) + 32)
-      unix_error (EINVAL, "getattrlist", path);
+      caml_unix_error(EINVAL, "getattrlist", path);
   }
 
   fInfo = caml_alloc_string (32);
   memcpy ((char *) String_val (fInfo), attrBuf.finderInfo, 32);
   if (Bool_val (need_size))
-    length = copy_int64 (attrBuf.rsrcLength);
+    length = caml_copy_int64(attrBuf.rsrcLength);
   else
-    length = copy_int64 (0);
+    length = caml_copy_int64(0);
 
-  res = alloc_small (2, 0);
+  res = caml_alloc_small(2, 0);
   Field (res, 0) = fInfo;
   Field (res, 1) = length;
 
@@ -79,7 +84,7 @@ CAMLprim value getFileInfos (value path, value need_size) {
 
 #else
 
-  unix_error (ENOSYS, "getattrlist", path);
+  caml_unix_error(ENOSYS, "getattrlist", path);
 
 #endif
 }
@@ -114,9 +119,9 @@ CAMLprim value setFileInfos (value path, value fInfo) {
        if file is read-only.  Try making it writable temporarily. */
     struct stat st;
     int r = stat(String_val(path), &st);
-    if (r == -1) uerror("setattrlist", path);
+    if (r == -1) caml_uerror("setattrlist", path);
     r = chmod(String_val(path), st.st_mode | S_IWUSR);
-    if (r == -1) uerror("setattrlist", path);
+    if (r == -1) caml_uerror("setattrlist", path);
     /* Try again */
     retcode = setattrlist(String_val (path), &attrList, attrBuf.finderInfo,
                           sizeof attrBuf.finderInfo, options);
@@ -124,13 +129,13 @@ CAMLprim value setFileInfos (value path, value fInfo) {
     chmod(String_val(path), st.st_mode);
   }
 
-  if (retcode == -1) uerror("setattrlist", path);
+  if (retcode == -1) caml_uerror("setattrlist", path);
 
   CAMLreturn (Val_unit);
 
 #else
 
-  unix_error (ENOSYS, "setattrlist", path);
+  caml_unix_error(ENOSYS, "setattrlist", path);
 
 #endif
 }
diff --git a/src/path.ml b/src/path.ml
index 147fee2..1970734 100644
--- a/src/path.ml
+++ b/src/path.ml
@@ -22,6 +22,10 @@ type 'a path = string
 type t = string
 type local = string
 
+let mpath = Umarshal.string
+let m = mpath
+let mlocal = mpath
+
 let pathSeparatorChar = '/'
 let pathSeparatorString = "/"
 
@@ -211,7 +215,8 @@ let addPrefixToFinalName path prefix =
     prefix ^ path
 
 (* Pref controlling whether symlinks are followed. *)
-let followPred = Pred.create ~advanced:true "follow"
+let followPred = Pred.create "follow"
+    ~category:(`Advanced `Sync)
     ("Including the preference \\texttt{-follow \\ARG{pathspec}} causes Unison to \
       treat symbolic links matching \\ARG{pathspec} as `invisible' and \
       behave as if the object pointed to by the link had appeared literally \
@@ -221,8 +226,7 @@ let followPred = Pred.create ~advanced:true "follow"
       described in \\sectionref{pathspec}{Path Specification}.")
 
 let followLink path =
-     (Util.osType = `Unix || Util.isCygwin)
-  && Pred.test followPred (toString path)
+  Pred.test followPred (toString path)
 
 let forceLocal p = p
 let makeGlobal p = p
diff --git a/src/path.mli b/src/path.mli
index 8c6cf9a..f01356d 100644
--- a/src/path.mli
+++ b/src/path.mli
@@ -12,6 +12,10 @@ type t = [`Global] path
    sensitive filesystem) *)
 type local = [`Local] path
 
+val mpath : 'a path Umarshal.t
+val m : t Umarshal.t
+val mlocal : local Umarshal.t
+
 val empty : 'a path
 val length : t -> int
 val isEmpty : local -> bool
diff --git a/src/pixmaps.ml b/src/pixmaps.ml
index 857995f..1eeb213 100644
--- a/src/pixmaps.ml
+++ b/src/pixmaps.ml
@@ -251,23 +251,6 @@ let copyBAblack_asym = [|
 "............................"
 |]
 
-(***********************************************************************)
-(*                      Busy-Interactive mous pointer                  *)
-(***********************************************************************)
-
-let left_ptr_watch = "\
-\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\
-\x0c\x00\x00\x00\x1c\x00\x00\x00\x3c\x00\x00\x00\
-\x7c\x00\x00\x00\xfc\x00\x00\x00\xfc\x01\x00\x00\
-\xfc\x3b\x00\x00\x7c\x38\x00\x00\x6c\x54\x00\x00\
-\xc4\xdc\x00\x00\xc0\x44\x00\x00\x80\x39\x00\x00\
-\x80\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\
-\x00\x00\x00\x00\x00\x00\x00\x00"
-
 
 (***********************************************************************)
 (*                          Unison icon                                *)
diff --git a/src/pred.ml b/src/pred.ml
index 10fd578..520f5ca 100644
--- a/src/pred.ml
+++ b/src/pred.ml
@@ -98,16 +98,15 @@ let compile_pattern clause =
     end in
   (compiled, v)
 
-let create name ?(local=false) ?(advanced=false) fulldoc =
+let create name ~category ?(local=false) ?send ?(initial = []) fulldoc =
   let pref =
-    Prefs.create name ~local []
-      ((if advanced then "!" else "")
-       ^ "add a pattern to the " ^ name ^ " list")
+    Prefs.create name ~category ~local ?send initial
+      ("add a pattern to the " ^ name ^ " list")
       fulldoc
       (fun oldList string ->
          ignore (compile_pattern string); (* Check well-formedness *)
         string :: oldList)
-      (fun l -> l) in
+      (fun l -> l) Umarshal.(list string) in
   {pref = pref; name = name;
    last_pref = []; default = []; last_def = []; last_mode = (Case.ops())#mode;
    compiled = Rx.empty; associated_strings = []}
diff --git a/src/pred.mli b/src/pred.mli
index 7329988..cd915b2 100644
--- a/src/pred.mli
+++ b/src/pred.mli
@@ -30,9 +30,22 @@ type t
 
 val mapSeparator : string
 
-(* Create a new predicate and register it with the preference module.  The first
-   arg is the name of the predicate; the second is full (latex) documentation. *)
-val create : string -> ?local:bool -> ?advanced:bool -> string -> t
+(* Create a new predicate and register it with the preference module. *)
+val create :
+    string               (* Name of the predicate *)
+ -> category:Prefs.group
+ -> ?local:bool
+ -> ?send:(unit -> bool)
+ -> ?initial:string list (* Initial value for the "current patterns", separate
+                            from the persistent default patterns that are
+                            modified by [addDefaultPatterns]. User preferences
+                            will be added to this value, but this value is not
+                            persistent when the associated preference is cleared
+                            (for example, [intern] will overwrite it). This
+                            value will be returned by [extern] (if it hasn't
+                            been cleared before). *)
+ -> string               (* Full (latex) documentation *)
+ -> t
 
 (* Check whether a given path matches one of the default or current patterns *)
 val test : t -> string -> bool
diff --git a/src/props.ml b/src/props.ml
index 4a502e7..469d621 100644
--- a/src/props.ml
+++ b/src/props.ml
@@ -17,9 +17,11 @@
 
 
 let debug = Util.debug "props"
+let debugverbose = Util.debug "props+"
 
 module type S = sig
   type t
+  val m : t Umarshal.t
   val dummy : t
   val hash : t -> int -> int
   val similar : t -> t -> bool
@@ -28,9 +30,8 @@ module type S = sig
   val diff : t -> t -> t
   val toString : t -> string
   val syncedPartsToString : t -> string
-  val set : Fspath.t -> Path.local -> [`Set | `Update] -> t -> unit
-  val get : Unix.LargeFile.stats -> Osx.info -> t
-  val init : bool -> unit
+  val set : Fspath.t -> t -> unit
+  val get : Unix.LargeFile.stats -> t
 end
 
 (* Nb: the syncedPartsToString call is only used for archive dumping, for    *)
@@ -44,10 +45,12 @@ module Perm : sig
   val fileSafe : t
   val dirDefault : t
   val extract : t -> int
+  val set : Fspath.t -> [`Set | `Update] -> t -> unit
   val check : Fspath.t -> Path.local -> Unix.LargeFile.stats -> t -> unit
   val validatePrefs : unit -> unit
   val permMask : int Prefs.t
   val dontChmod : bool Prefs.t
+  val init : bool -> unit
 end = struct
 
 (* We introduce a type, Perm.t, that holds a file's permissions along with   *)
@@ -66,6 +69,8 @@ end = struct
 (* "UPDATE.ARCHIVEFORMAT"                                                    *)
 type t = int * int
 
+let m = Umarshal.(prod2 int int id id)
+
 (* This allows us to export NullPerm while keeping the type perm abstract    *)
 let dummy = (0, 0)
 
@@ -79,17 +84,18 @@ let wind_mask =
 let permMask =
   Prefs.createInt "perms"
     (0o777 (* rwxrwxrwx *) + 0o1000 (* Sticky bit *))
+    ~category:(`Basic `Sync)
     "part of the permissions which is synchronized"
     "The integer value of this preference is a mask indicating which \
      permission bits should be synchronized.  It is set by default to \
      $0o1777$: all bits but the set-uid and set-gid bits are \
-     synchronised (synchronizing theses latter bits can be a security \
+     synchronised (synchronizing these latter bits can be a security \
      hazard).  If you want to synchronize all bits, you can set the \
      value of this preference to $-1$.  If one of the replica is on \
      a FAT [Windows] filesystem, you should consider using the \
-     {\tt fat} preference instead of this preference.  If you need \
+     {\\tt fat} preference instead of this preference.  If you need \
      Unison not to set permissions at all, set the value of this \
-     preference to $0$ and set the preference {\tt dontchmod} to {\tt true}."
+     preference to $0$ and set the preference {\\tt dontchmod} to {\\tt true}."
 
 (* Os-specific local conventions on file permissions                         *)
 let (fileDefault, dirDefault, fileSafe, dirSafe) =
@@ -194,7 +200,8 @@ let syncedPartsToString =
 let dontChmod =
   Prefs.createBool "dontchmod"
   false
-  "!when set, never use the chmod system call"
+  ~category:(`Advanced `Syncprocess)
+  "when set, never use the chmod system call"
   (  "By default, Unison uses the 'chmod' system call to set the permission bits"
   ^ " of files after it has copied them.  But in some circumstances (and under "
   ^ " some operating systems), the chmod call always fails.  Setting this "
@@ -204,7 +211,7 @@ let validatePrefs () =
   if Prefs.read dontChmod && (Prefs.read permMask <> 0) then raise (Util.Fatal
     "If the 'dontchmod' preference is set, the 'perms' preference should be 0")
 
-let set fspath path kind (fp, mask) =
+let set abspath kind (fp, mask) =
   (* BCP: removed "|| kind <> `Update" on 10/2005, but reinserted it on 11/2008.
      I'd removed it to make Dale Worley happy -- he wanted a way to make sure that
      Unison would never call chmod, and setting prefs to 0 seemed like a reasonable
@@ -214,7 +221,6 @@ let set fspath path kind (fp, mask) =
     Util.convertUnixErrorsToTransient
     "setting permissions"
       (fun () ->
-        let abspath = Fspath.concat fspath path in
         debug
           (fun() ->
             Util.msg "Setting permissions for %s to %s (%s)\n"
@@ -233,7 +239,7 @@ let set fspath path kind (fp, mask) =
                        or else set preference \"perms\" to 0 and \
                        preference \"dontchmod\" to true to avoid this error")))
 
-let get stats _ = (stats.Unix.LargeFile.st_perm, Prefs.read permMask)
+let get stats = (stats.Unix.LargeFile.st_perm, Prefs.read permMask)
 
 let check fspath path stats (fp, mask) =
   let fp' = stats.Unix.LargeFile.st_perm in
@@ -273,7 +279,8 @@ end
 
 let numericIds =
   Prefs.createBool "numericids" false
-    "!don't map uid/gid values by user/group names"
+    ~category:(`Advanced `Syncprocess)
+    "don't map uid/gid values by user/group names"
     "When this flag is set to \\verb|true|, groups and users are \
      synchronized numerically, rather than by name. \n\
      \n\
@@ -291,13 +298,26 @@ module Id (M : sig
   val syncedPartsToString : int -> string
   val set : Fspath.t -> int -> unit
   val get : Unix.LargeFile.stats -> int
-end) : S = struct
+end) : sig
+  include S
+  val init : bool -> unit
+end = struct
 
 type t =
     IdIgnored
   | IdNamed of string
   | IdNumeric of int
 
+let m = Umarshal.(sum3 unit string int
+                    (function
+                     | IdIgnored -> I31 ()
+                     | IdNamed a -> I32 a
+                     | IdNumeric a -> I33 a)
+                    (function
+                     | I31 () -> IdIgnored
+                     | I32 a -> IdNamed a
+                     | I33 a -> IdNumeric a))
+
 let dummy = IdIgnored
 
 let hash id h =
@@ -348,7 +368,7 @@ let extern id =
         Hashtbl.add tbl nm id;
         id
 
-let set fspath path kind id =
+let set abspath id =
   match extern id with
     -1 ->
       ()
@@ -356,12 +376,11 @@ let set fspath path kind id =
       Util.convertUnixErrorsToTransient
         "setting file ownership"
         (fun () ->
-           let abspath = Fspath.concat fspath path in
            M.set abspath id)
 
 let tbl = Hashtbl.create 17
 
-let get stats _ =
+let get stats =
   if not (Prefs.read M.sync) then IdIgnored else
   let id = M.get stats in
   if id = 0 || Prefs.read numericIds then IdNumeric id else
@@ -381,8 +400,9 @@ end
 module Uid = Id (struct
 
 let sync =
-  Prefs.createBool "owner"
-    false "synchronize owner"
+  Prefs.createBool "owner" false
+    ~category:(`Basic `Sync)
+    "synchronize owner"
     ("When this flag is set to \\verb|true|, the owner attributes "
      ^ "of the files are synchronized.  "
      ^ "Whether the owner names or the owner identifiers are synchronized"
@@ -402,8 +422,9 @@ end)
 module Gid = Id (struct
 
 let sync =
-  Prefs.createBool "group"
-    false "synchronize group attributes"
+  Prefs.createBool "group" false
+    ~category:(`Basic `Sync)
+    "synchronize group attributes"
     ("When this flag is set to \\verb|true|, the group attributes "
      ^ "of the files are synchronized.  "
      ^ "Whether the group names or the group identifiers are synchronized "
@@ -434,13 +455,22 @@ module Time : sig
 end = struct
 
 let sync =
-  Prefs.createBool "times"
-    false "synchronize modification times"
+  Prefs.createBool "times" false
+    ~category:(`Basic `Sync)
+    "synchronize modification times"
     "When this flag is set to \\verb|true|, \
      file modification times (but not directory modtimes) are propagated."
 
 type t = Synced of float | NotSynced of float
 
+let m = Umarshal.(sum2 float float
+                    (function
+                     | Synced a -> I21 a
+                     | NotSynced a -> I22 a)
+                    (function
+                     | I21 a -> Synced a
+                     | I22 a -> NotSynced a))
+
 let dummy = NotSynced 0.
 
 let extract t = match t with Synced v -> v | NotSynced v -> v
@@ -491,7 +521,7 @@ let override t t' =
 
 let replace t v =
   match t with
-    Synced _    -> t
+    Synced _    -> Synced v
   | NotSynced _ -> NotSynced v
 
 let strip t =
@@ -509,36 +539,13 @@ let syncedPartsToString t = match t with
 
 (* FIX: Probably there should be a check here that prevents us from ever     *)
 (* setting a file's modtime into the future.                                 *)
-let set fspath path kind t =
+let set abspath t =
   match t with
     Synced v ->
       Util.convertUnixErrorsToTransient
         "setting modification time"
         (fun () ->
-           let abspath = Fspath.concat fspath path in
-           if not (Fs.canSetTime abspath) then
-             begin
-              (* Nb. This workaround was proposed by Dmitry Bely, to
-                 work around the fact that Unix.utimes fails on readonly
-                 files under windows.  I'm [bcp] a little bit uncomfortable
-                 with it for two reasons: (1) if we crash in the middle,
-                 the permissions might be left in a bad state, and (2) I
-                 don't understand the Win32 permissions model enough to
-                 know whether it will always work -- e.g., what if the
-                 UID of the unison process is not the same as that of the
-                 file itself (under Unix, this case would fail, but we
-                 certainly don't want to make it WORLD-writable, even
-                 briefly!). *)
-               let oldPerms =
-                 (Fs.lstat abspath).Unix.LargeFile.st_perm in
-               Util.finalize
-                 (fun()->
-                    Fs.chmod abspath 0o600;
-                    Fs.utimes abspath (if v = 0. then 1e-12 else v) v)
-                    (* See comment about this statement further below *)
-                 (fun()-> Fs.chmod abspath oldPerms)
-             end
-           else if false then begin
+           if false then begin
              (* A special hack for Rasmus, who has a special situation that
                 requires the utimes-setting program to run 'setuid root'
                 (and we do not want all of Unison to run setuid, so we just
@@ -555,7 +562,7 @@ let set fspath path kind t =
              let cmd = "/usr/local/bin/sudo -u root /usr/bin/touch -m -a -t "
                        ^ tstr ^ " " ^ Fspath.quotes abspath in
              Util.msg "Running external program to set utimes:\n  %s\n" cmd;
-             let (r,_) = Lwt_unix.run (External.runExternalProgram cmd) in
+             let r = System.close_process_in (System.open_process_in cmd) in
              if r<>(Unix.WEXITED 0) then raise (Util.Transient "External time-setting command failed")
            end else
              Fs.utimes abspath (if v = 0. then 1e-12 else v) v)
@@ -569,7 +576,7 @@ let set fspath path kind t =
   | _ ->
       ()
 
-let get stats _ =
+let get stats =
   let v = stats.Unix.LargeFile.st_mtime in
   if stats.Unix.LargeFile.st_kind = Unix.S_REG && Prefs.read sync then
     Synced v
@@ -603,18 +610,23 @@ let same p p' =
       let delta = extract p -. extract p' in
       delta = 0. || delta = 3600. || delta = -3600.
 
-let init _ = ()
-
 end
 
 (* ------------------------------------------------------------------------- *)
 (*                          Type and creator                                 *)
 (* ------------------------------------------------------------------------- *)
 
-module TypeCreator : S = struct
+module TypeCreator :
+  sig
+    include S
+    val set : Fspath.t -> Path.local -> t -> unit
+    val get : Unix.LargeFile.stats -> Osx.info -> t
+  end = struct
 
 type t = string option
 
+let m = Umarshal.(option string)
+
 let dummy = None
 
 let hash t h = Uutil.hash2 (Uutil.hash t) h
@@ -642,7 +654,7 @@ let toString t =
 
 let syncedPartsToString = toString
 
-let set fspath path kind t =
+let set fspath path t =
   match t with
     None   -> ()
   | Some t -> Osx.setFileInfos fspath path t
@@ -657,7 +669,612 @@ let get stats info =
   else
     None
 
-let init _ = ()
+end
+
+(* ------------------------------------------------------------------------- *)
+(*                               Change time                                 *)
+(* ------------------------------------------------------------------------- *)
+
+(* ctime itself is never synchronized. It is only leveraged for faster
+   metadata update detection; and stored in archive for this purpose. *)
+
+module CTime : sig
+  type t
+  val m : t Umarshal.t
+  val dummy : t
+  val override : t -> t -> t
+  val get : Unix.LargeFile.stats -> t
+  val same_time : t -> t -> bool
+end = struct
+
+type t = float
+
+let m = Umarshal.float
+
+let dummy = -1.
+
+(* Currently [override] does not work for ctime because the real on-disk
+   ctime will inevitably change when the final props are set on disk by
+   [Files.setProp] or the final rename after copying is done in [Files.copy]
+   (these happen after [override]). There is no [stat] done after these
+   operations, so this final ctime will not get stored in the archive.
+   It is not a major issue and doesn't break anything. The only side-effect is
+   that at next updates scan the entire set of metadata for this file/dir is
+   scanned (as if fastcheck was disabled); which may even be a good thing.
+   Not worth changing or adding the cost of an additional [stat]. But if it
+   is changed in future then the proper ctime value must be extracted in
+   [Props.get']. *)
+let override t t' = t
+
+let get stats = stats.Unix.LargeFile.st_ctime
+
+let same_time t t' = System.hasCorrectCTime && t = t'
+
+end
+
+(* ------------------------------------------------------------------------- *)
+(*                        Extended attributes (xattr)                        *)
+(* ------------------------------------------------------------------------- *)
+
+let featXattrValid = ref (fun _ _ -> None)
+
+let featXattr =
+  Features.register "Sync: xattr" ~arcFormatChange:true
+  (Some (fun a b -> !featXattrValid a b))
+
+let xattrEnabled () = Features.enabled featXattr
+
+let syncXattrs =
+  Prefs.createBool "xattrs" false
+    ~category:(`Advanced `Sync)
+    ~send:xattrEnabled
+    "synchronize extended attributes (xattrs)"
+    ("When this flag is set to \\verb|true|, the extended attributes of \
+     files and directories are synchronized. System extended attributes \
+     are not synchronized.")
+
+let () = featXattrValid :=
+  fun _ enabledThis ->
+    if not enabledThis && Prefs.read syncXattrs then
+      Some ("You have requested synchronization of extended attributes (the \
+        \"xattrs\" preference) but the server does not support this.")
+    else None
+
+let xattrIgnorePred =
+  Pred.create "xattrignore"
+    ~category:(`Advanced `Sync)
+    ~send:xattrEnabled
+    (* By default ignore the Linux xattr security and trusted namespaces *)
+    ~initial:["Regex !(security|trusted)[.].*"; "Path !system.posix_acl_*"]
+    ("Preference \\texttt{-xattrignore \\ARG{namespec}} causes Unison to \
+     ignore extended attributes with names that match \\ARG{namespec}. \
+     This can be used to exclude extended attributes that would fail \
+     synchronization due to lack of permissions or technical differences \
+     at replicas. The syntax of \\ARG{namespec} is the same as used \
+     for path specification (described in \
+     \\sectionref{pathspec}{Path Specification}); prefer the \\verb|Path| \
+     and \\verb|Regex| forms over the \\verb|Name| form. The pattern is \
+     applied to the {\\em name} of extended attribute, not to path. \
+     {\\em On Linux}, attributes in the security and trusted namespaces \
+     are ignored by default (this is achieved by pattern \\texttt{Regex \
+     !(security|trusted)[.].*}); also attributes used to store POSIX ACL \
+     are ignored by default (this is achieved by pattern \\texttt{Path \
+     !system.posix\\_acl\\_*}). To sync attributes in one or both of \
+     these namespaces, see the \\verb|xattrignorenot| preference. \
+     Note that the namespace name must be prefixed with a \"!\" (applies \
+     on Linux only). All names not prefixed with a \"!\" are taken \
+     as strictly belonging to the user namespace and therefore the \
+     \"!user.\" prefix is never used.")
+
+let xattrIgnorenotPred =
+  Pred.create "xattrignorenot"
+    ~category:(`Advanced `Sync)
+    ~send:xattrEnabled
+    ("This preference overrides the preference \\texttt{xattrignore}. \
+     It gives a list of patterns (in the same format as \
+     \\verb|xattrignore|) for extended attributes that should {\\em not} \
+     be ignored, whether or not they happen to match one of the \
+     \\verb|xattrignore| patterns. It is possible to synchronize only \
+     desired attributes by ignoring all attributes (for example, by \
+     setting \\verb|xattrignore| to \\texttt{Path *} and then adding \
+     \\verb|xattrignorenot| for extended attributes that should be \
+     synchronized. \
+     {\\em On Linux}, attributes in the security and trusted namespaces \
+     are ignored by default. To sync attributes in one or both of these \
+     namespaces, you may add an \\verb|xattrignorenot| pattern like \
+     \\texttt{Path !security.*} to sync all attributes in the \
+     security namespace, or \\texttt{Path !security.selinux} to sync \
+     a specific attribute in an otherwise ignored namespace. A pattern \
+     like \\texttt{Path !system.posix\\_acl\\_*} can be used to sync \
+     POSIX ACLs on Linux. \
+     Note that the namespace name must be prefixed with a \"!\" (applies \
+     on Linux only). All names not prefixed with a \"!\" are taken \
+     as strictly belonging to the user namespace and therefore the \
+     \"!user.\" prefix is never used.")
+
+module Xattr : sig
+  include S
+  val ctimeDetect : bool
+  val get : Fspath.t -> Unix.LargeFile.stats -> t
+  val readAll : Fspath.t -> t -> t
+  val getAll : t -> t
+  val purge : t -> t
+  val check : Fspath.t -> Path.local -> Unix.LargeFile.stats -> t -> unit
+  module Data : Propsdata.S
+end = struct
+
+module Size = Uutil.Filesize
+
+module Data = Propsdata.Xattr
+
+module Cache = struct
+  let get key = Data.find_opt key
+
+  let add key value =
+    (* Cache relatively small data in a relatively small quantity to keep
+       the memory pressure and network traffic at updates scanning low.
+
+       There is no cache management. Once it's full, it's full. This can be
+       enhanced in future, if needed. *)
+    if String.length value < 1024 && Data.length () < 200 then
+      Data.add key value;
+    value
+end
+
+type attrvalue =
+  | String of string
+  | Hash of string
+  | Loaded of (string * string) (* full value, hash *)
+
+let mattrvalue = Umarshal.(sum3 string string (prod2 string string id id)
+                            (function
+                             | String v -> I31 v
+                             | Hash v -> I32 v
+                             | Loaded v -> I33 v)
+                            (function
+                             | I31 v -> String v
+                             | I32 v -> Hash v
+                             | I33 v -> Loaded v))
+
+type attrlist = (string * attrvalue) list
+
+let mattrlist = Umarshal.(list (prod2 string mattrvalue id id))
+
+type sizeandattrs = attrlist * Uutil.Filesize.t
+
+let msizeandattrs = Umarshal.(prod2 mattrlist Uutil.Filesize.m id id)
+
+(* None indicates xattrs are not supported. This is not synchronized.
+ * An empty list means xattrs are supported but there are none on the file.
+ * This will be synchronized. *)
+type t = sizeandattrs option
+
+let dummy = None
+
+let m = Umarshal.cond xattrEnabled dummy Umarshal.(option msizeandattrs)
+
+let ctimeDetect = System.xattrUpdatesCTime
+
+(* Since [hash] is supposed to be run after [purge] (resulting in the
+   data that is stored in the archives) then we don't need to take
+   into account the difference between Hash and Loaded.
+
+   The attribute list must be sorted to get a stable hash. The list
+   is sorted once, when retrieving it from fs. If sorting conditions
+   are changed in future then this hash function may have to be
+   changed to retain backwards compatibility. *)
+let hash t h = if Prefs.read syncXattrs then Uutil.hash2 (Uutil.hash t) h else h
+
+let attrToString = function
+  | (n, String v) ->
+      Printf.sprintf "Name: %s    Value: %s" n (String.escaped v)
+  | (n, Hash h) ->
+      Printf.sprintf "Name: %s    Fingerprint: %s" n (Digest.to_hex h)
+  | (n, Loaded (_, h)) ->
+      Printf.sprintf "Name: %s    Fingerprint: %s" n (Digest.to_hex h)
+
+let toString' style = function
+  | Some ([], _) -> "0 xattrs"
+  | Some ([(n, _) as x], z) ->
+      Printf.sprintf "1 xattr (%s bytes)%s" (Size.toString z)
+        (match style with
+        | `Summary -> ""
+        | `Simple -> ": " ^ n
+        | `Verbose -> ": " ^ attrToString x)
+  | Some (l, z) ->
+      Printf.sprintf "%u xattrs (%s bytes)%s" (Safelist.length l) (Size.toString z)
+        (match style with
+        | `Summary -> ""
+        | `Simple -> ": " ^ (String.concat ", " (Safelist.map (fun (n, _) -> n) l))
+        | `Verbose -> "\n  " ^ (String.concat "\n  " (Safelist.map attrToString l)))
+  | None -> ""
+
+let toString = function
+  | None -> ""
+  | t -> " " ^ toString' `Summary t
+
+let syncedPartsToString t = " " ^ toString' `Simple t
+
+let toDebugString t = toString' `Simple t
+
+let toStringVerb t = toString' `Verbose t
+
+let attrEqual (n, v) (n', v') =
+  String.equal n n' &&
+  match v, v' with
+  | String a, String b
+  | String a, Loaded (b, _)
+  | Hash a, Hash b
+  | Hash a, Loaded (_, b)
+  | Loaded (a, _), String b
+  | Loaded (_, a), Hash b
+  | Loaded (_, a), Loaded (_, b) -> String.equal a b
+  | String s, Hash h
+  | Hash h, String s -> String.equal h (Digest.string s)
+
+let rec attrlist_mem x = function
+  | [] -> false
+  | a :: l -> attrEqual a x || attrlist_mem x l
+
+let similar t t' =
+  not (Prefs.read syncXattrs)
+    ||
+  match t, t' with
+  | None, None -> true
+  | Some (l, z), Some (l', z') ->
+      Int64.equal (Size.toInt64 z) (Size.toInt64 z') &&
+      Safelist.length l = Safelist.length l' &&
+        Safelist.for_all (fun m -> attrlist_mem m l') l
+  | _ -> false
+
+let override t t' = t'
+
+let strip t = if Prefs.read syncXattrs then t else None
+
+let diff t t' = if similar t t' then None else t'
+
+let wrapFail default f =
+  try f () with
+  | Fs.XattrNotSupported -> default
+  | Failure msg ->
+      raise (Util.Transient (msg ^
+        ". You can set preference \"xattrs\" to false to avoid this error."))
+
+let optMap f = function None -> None | Some x -> Some (f x)
+let optAttrsMap f = optMap (fun (l, z) -> (Safelist.map f l, z))
+
+let purge t =
+  optAttrsMap (function (n, Loaded (_, h)) -> (n, Hash h) | x -> x) t
+
+let readAll path t =
+  let f = function
+    | (n, Hash h) ->
+        debugverbose (fun () ->
+          Util.msg "Reading xattr %s for %s\n" n (Fspath.toDebugString path));
+        let v' =
+          match Cache.get h with
+          | Some v ->
+              debugverbose (fun () -> Util.msg "Read xattr %s from cache\n" n);
+              v
+          | None ->
+              let v = Fs.xattr_get path n in
+              if Digest.string v <> h then
+                raise (Util.Transient (
+                  Printf.sprintf "The value of extended attribute '%s' has \
+                    changed on source file %s" n (Fspath.toPrintString path)))
+              else
+                Cache.add h v
+        in
+        (n, Loaded (v', h))
+    | x -> x
+  in
+  if Prefs.read syncXattrs then
+    wrapFail t (fun () -> optAttrsMap f t)
+  else
+    t
+
+let getAll t =
+  let f = function
+    | (n, Hash h) ->
+        begin match Cache.get h with
+        | Some v ->
+            debugverbose (fun () -> Util.msg "Got xattr %s from cache\n" n);
+            (n, Loaded (v, h))
+        | None -> raise Not_found
+        end
+    | x -> x
+  in
+  if Prefs.read syncXattrs then
+    wrapFail t (fun () -> optAttrsMap f t)
+  else
+    t
+
+let skipIgnoredXattr l =
+  Safelist.filter (fun (n, _) ->
+    let keep =
+      not (Pred.test xattrIgnorePred n) || (Pred.test xattrIgnorenotPred n) in
+    debugverbose (fun () ->
+      Util.msg "Xattr: attribute %s %s\n" n
+        (if keep then "not ignored" else "IGNORED by user request"));
+    keep) l
+
+let getXattrs path =
+  let sumSize total (_, len) = total + len in (* No fear of overflow *)
+  let xattrNameCompare (a, _) (b, _) = String.compare a b in
+  let sortXattrs = Safelist.sort xattrNameCompare in
+  let readXattr (n, len) =
+    if len > 16777211 then (* Max length of strings on 32-bit OCaml *)
+      failwith ("The value of extended attribute '" ^ n ^
+        "' is larger than 16 MB. This is currently not supported") else
+    let v = Fs.xattr_get path n in
+    let value =
+      if len <= 32 then String v
+      else
+        let h = Digest.string v in
+        let _ = Cache.add h v in
+        Hash h
+    in
+    (n, value)
+  in
+  wrapFail None (fun () ->
+    let names = Fs.xattr_list path |> skipIgnoredXattr |> sortXattrs in
+    let size = Size.ofInt (Safelist.fold_left sumSize 0 names) in
+    Some (Safelist.map readXattr names, size))
+
+let setXattrs path t =
+  match t with
+  | Some (l, _) -> begin
+      match getXattrs path with
+      | Some (xattrs0, _) -> begin
+          try
+            let xattrs = skipIgnoredXattr l in
+            xattrs |> Safelist.iter (fun ((n, v) as m) ->
+              if not (attrlist_mem m xattrs0) then
+              begin
+                debugverbose (fun () -> Util.msg "Writing xattr: %s\n" n);
+                match v with
+                | String x | Loaded (x, _) -> Fs.xattr_set path n x
+                | Hash _ -> () (* This should not happen; just skip it *)
+              end);
+            xattrs0 |> Safelist.iter (fun (n, _) ->
+              if not (Safelist.exists (fun (n', _) -> n' = n) xattrs) then
+              begin
+                debugverbose (fun () -> Util.msg "Removing xattr: %s\n" n);
+                Fs.xattr_remove path n
+              end)
+          with
+          | Fs.XattrNotSupported ->
+              raise (Util.Transient ("Extended attributes are not supported. \
+                       You can set preference \"xattrs\" to false \
+                       to avoid this error."))
+          | Failure msg ->
+              raise (Util.Transient (msg ^
+                       ". You can set preference \"xattrs\" to false \
+                       to avoid this error. You can add a 'debug' preference \
+                       with value \"props+\" to see more details."))
+        end
+      | _ -> ()
+    end
+  | _ -> ()
+
+let set abspath t =
+  match t with
+  | Some _ when Prefs.read syncXattrs ->
+      debug (fun () ->
+        Util.msg "Setting xattrs for %s (%s)\n"
+          (Fspath.toDebugString abspath) (toDebugString t));
+      setXattrs abspath t
+  | _ -> ()
+
+let get abspath stats =
+  if Prefs.read syncXattrs &&
+    (stats.Unix.LargeFile.st_kind = Unix.S_REG ||
+     stats.Unix.LargeFile.st_kind = Unix.S_DIR)
+    (* Theoretically could sync xattrs on symlinks (if C stubs are
+       enhanced accordingly). However, in the current implementation
+       there are no props stored for symlinks in the archive. *)
+  then
+    let xattrs = getXattrs abspath in
+    debug (fun () ->
+      Util.msg "Xattr: got %s for %s\n"
+        (toDebugString xattrs) (Fspath.toDebugString abspath));
+    xattrs
+  else
+    None
+
+let check fspath path stats t =
+  match t with
+  | None -> ()
+  | Some _ ->
+      let abspath = Fspath.concat fspath path in
+      let t' = get abspath stats in
+      if not (similar t t') then
+        let msg = Format.sprintf ("Failed to set requested extended attributes \
+          on %s.\nThe following attributes were requested to be set:\n%s\n\
+          Actual attributes after setting:\n%s")
+          (Fspath.toPrintString abspath) (toStringVerb t) (toStringVerb t') in
+        raise (Util.Transient msg)
+
+end
+
+(* ------------------------------------------------------------------------- *)
+(*                                   ACL                                     *)
+(* ------------------------------------------------------------------------- *)
+
+let featACLValid = ref (fun _ _ -> None)
+
+let featACL =
+  Features.register "Sync: ACL" ~arcFormatChange:true
+  (Some (fun a b -> !featACLValid a b))
+
+let aclEnabled () = Features.enabled featACL
+
+let syncACL =
+  Prefs.createBool "acl" false
+    ~category:(`Advanced `Sync)
+    ~send:aclEnabled
+    "synchronize ACLs"
+    ("When this flag is set to \\verb|true|, the ACLs of files and \
+     directories are synchronized. The type of ACLs depends on the \
+     platform and filesystem support. On Unix-like platforms it \
+     can be NFSv4 ACLs, for example.")
+
+let () = featACLValid :=
+  fun _ enabledThis ->
+    if not enabledThis && Prefs.read syncACL then
+      Some ("You have requested synchronization of ACLs (the \
+        \"acl\" preference) but the server does not support this.")
+    else None
+
+module ACL : sig
+  include S
+  val get : Fspath.t -> Unix.LargeFile.stats -> t
+  val check : Fspath.t -> Path.local -> Unix.LargeFile.stats -> t -> unit
+  module Data : sig
+    include Propsdata.S
+    val keep : t -> unit
+  end
+end = struct
+
+module Data = struct
+  include Propsdata.ACL
+
+  let keep = function
+    | None | Some "" -> ()
+    | Some s -> keep s (* [keep] of Propsdata.ACL *)
+end
+
+(* The result value of this function must be deterministic for its input
+   (over both roots, and over time, as long as it is the same archive). *)
+let deflate acl =
+  if acl = "" then acl
+  else begin
+    let key = Digest.string acl in
+    Data.add key acl;
+    key
+  end
+
+let inflate t = if t = "" then t else Data.find t
+
+(* None indicates ACLs are not supported. This is not synchronized.
+   An empty string represents a trivial/removed ACL. This will be
+   synchronized. *)
+type t = string option
+
+let dummy = None
+
+let m = Umarshal.cond aclEnabled dummy Umarshal.(option string)
+
+let hash t h = if Prefs.read syncACL then Uutil.hash2 (Uutil.hash t) h else h
+
+let toString = function
+  | Some "" -> " <trivial ACL>"
+  | Some s -> " A=" ^ (inflate s)
+  | None -> if not (Prefs.read syncACL) then "" else " !No ACL support!"
+
+let syncedPartsToString = toString
+
+let aclIds = Str.regexp
+  "\\(\\(user\\|group\\):\\)[^:]+:\\([^:]+:[^:]+:[^:]+:[0-9]+\\($\\|,\\)\\)"
+let removeAclNames s =
+  Str.global_replace aclIds "\\1\\3" (inflate s)
+
+let similar2 t t' =
+  Prefs.read numericIds
+    &&
+  (* Try to strip out the user/group names and compare only numeric ids.
+     Format of ACE is expected to be as follows:
+       user:name:rw------------:------I:allow:1300 *)
+  String.equal (removeAclNames t) (removeAclNames t')
+
+let similar t t' =
+  not (Prefs.read syncACL)
+    ||
+  (* This is a direct string comparison. It does not take into account
+     changes in ACE ordering because ACE ordering is considered to be
+     significant and different ordering means different ACL. *)
+  let result =
+    match t, t' with
+    | None, None -> true
+    | Some acl, Some acl' when String.equal acl acl' -> true
+    | Some acl, Some acl' -> similar2 acl acl'
+    | _ -> false in
+  debugverbose (fun () ->
+    Util.msg "Comparing ACLs |%s| and |%s| => %s%s\n"
+      (toString t) (toString t')
+      (match result with true -> "same" | false -> "different")
+      (if Prefs.read numericIds then
+        " (comparing numeric user/group ids)" else ""));
+  result
+
+let override t t' = t'
+
+let strip t = if Prefs.read syncACL then t else None
+
+let diff t t' = if similar t t' then None else t'
+
+let wrapFail f =
+  try f () with
+  | Failure msg ->
+      raise (Util.Transient (msg ^
+        ". You can set preference \"acl\" to false to avoid this error."))
+
+let getACLAsText path =
+  wrapFail (fun () ->
+    match Fs.acl_get_text path with
+    | "-1" -> None (* "-1" is used as a special code for no ACL support *)
+    | acl -> Some (deflate acl))
+
+let setACLFromText path t =
+  match t with
+  | Some acl -> wrapFail (fun () -> Fs.acl_set_text path (inflate acl))
+  | _ -> ()
+
+let set abspath t =
+  match t with
+  | Some _ when Prefs.read syncACL ->
+      debug (fun () ->
+        Util.msg "Setting ACL for %s from text |%s|\n"
+          (Fspath.toDebugString abspath) (toString t));
+      setACLFromText abspath t
+  | _ -> ()
+
+let get abspath stats =
+  if Prefs.read syncACL &&
+    (stats.Unix.LargeFile.st_kind = Unix.S_REG ||
+     stats.Unix.LargeFile.st_kind = Unix.S_DIR)
+    (* Theoretically could sync ACLs on symlinks (if C stubs are
+       enhanced accordingly). However, in the current implementation
+       there are no props stored for symlinks in the archive. *)
+  then
+    let acltext = getACLAsText abspath in
+    debug (fun () ->
+      Util.msg "Got text ACL |%s| for %s\n"
+        (toString acltext) (Fspath.toDebugString abspath));
+    acltext
+  else
+    None
+
+let check fspath path stats acl =
+  match acl with
+  | None -> ()
+  | Some _ ->
+      let abspath = Fspath.concat fspath path in
+      let acl' = get abspath stats in
+      if not (similar acl acl') then
+        let msg = Format.sprintf
+          "Failed to set ACL of file %s to\n%s\n\
+          The ACL was instead set to\n%s\n\
+          The filesystem probably does not have full ACL support or \
+          the synchronized ACL is of different type, or there \
+          are other incompatibilities between systems. \
+          If this is a filesystem without correct ACL support, you \
+          should set the \"acl\" preference to false.%s"
+          (Fspath.toPrintString abspath) (toString acl) (toString acl')
+          (if Prefs.read numericIds then "" else " Or, you may want to \
+             try setting the \"numericids\" preference to true if the \
+             user/group names don't match on both systems.") in
+        raise (Util.Transient msg)
 
 end
 
@@ -665,7 +1282,12 @@ end
 (*                           Properties                                      *)
 (* ------------------------------------------------------------------------- *)
 
-type t =
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [Props.t]. It must always remain
+   exactly the same as the type [Props.t] in version 2.51.5. This means that if
+   any of the types it is composed of changes then for each changed type also a
+   2.51-compatible version must be created. *)
+type t251 =
   { perm : Perm.t;
     uid : Uid.t;
     gid : Gid.t;
@@ -673,14 +1295,82 @@ type t =
     typeCreator : TypeCreator.t;
     length : Uutil.Filesize.t }
 
+type t =
+  { perm : Perm.t;
+    uid : Uid.t;
+    gid : Gid.t;
+    time : Time.t;
+    typeCreator : TypeCreator.t;
+    length : Uutil.Filesize.t;
+    ctime : CTime.t;
+    xattr : Xattr.t;
+    acl : ACL.t;
+  }
+
+type _ props = t
+type basic = [`Basic] props
+type x = [`ExtLoaded] props
+
+let m = Umarshal.(prod4
+                    (prod6 Perm.m Uid.m Gid.m Time.m TypeCreator.m Uutil.Filesize.m id id)
+                    (cond (fun () -> xattrEnabled () || aclEnabled ()) CTime.dummy CTime.m)
+                    Xattr.m
+                    ACL.m
+                    (fun {perm; uid; gid; time; typeCreator; length; ctime; xattr; acl} ->
+                       ((perm, uid, gid, time, typeCreator, length), ctime, xattr, acl))
+                    (fun ((perm, uid, gid, time, typeCreator, length), ctime, xattr, acl) ->
+                       {perm; uid; gid; time; typeCreator; length; ctime; xattr; acl}))
+
+let mbasic = m
+let mx = m
+
+let to_compat251 (p : t) : t251 =
+  { perm = p.perm;
+    uid = p.uid;
+    gid = p.gid;
+    time = p.time;
+    typeCreator = p.typeCreator;
+    length = p.length }
+
+let of_compat251 (p : t251) : t =
+  { perm = p.perm;
+    uid = p.uid;
+    gid = p.gid;
+    time = p.time;
+    typeCreator = p.typeCreator;
+    length = p.length;
+    ctime = CTime.dummy;
+    xattr = Xattr.dummy;
+    acl = ACL.dummy;
+  }
+
 let template perm =
   { perm = perm; uid = Uid.dummy; gid = Gid.dummy;
     time = Time.dummy; typeCreator = TypeCreator.dummy;
-    length = Uutil.Filesize.dummy }
+    length = Uutil.Filesize.dummy;
+    ctime = CTime.dummy;
+    xattr = Xattr.dummy;
+    acl = ACL.dummy;
+  }
 
 let dummy = template Perm.dummy
 
 let hash p h =
+  h
+  |> ACL.hash p.acl
+  |> Xattr.hash p.xattr
+  |> TypeCreator.hash p.typeCreator
+  |> Time.hash p.time
+  |> Gid.hash p.gid
+  |> Uid.hash p.uid
+  |> Perm.hash p.perm
+
+(* IMPORTANT!
+   This is the 2.51-compatible version of [hash]. It must always produce exactly
+   the same result as the [hash] in version 2.51.5.
+   If code changes elsewhere make this function produce a different result then
+   it must be updated accordingly to again return the 2.51-compatible result. *)
+let hash251 (p : t251) h =
   Perm.hash p.perm
     (Uid.hash p.uid
        (Gid.hash p.gid
@@ -697,6 +1387,10 @@ let similar p p' =
   Time.similar p.time p'.time
     &&
   TypeCreator.similar p.typeCreator p'.typeCreator
+    &&
+  Xattr.similar p.xattr p'.xattr
+    &&
+  ACL.similar p.acl p'.acl
 
 let override p p' =
   { perm = Perm.override p.perm p'.perm;
@@ -704,7 +1398,11 @@ let override p p' =
     gid = Gid.override p.gid p'.gid;
     time = Time.override p.time p'.time;
     typeCreator = TypeCreator.override p.typeCreator p'.typeCreator;
-    length = p'.length }
+    length = p'.length;
+    ctime = CTime.override p.ctime p'.ctime;
+    xattr = Xattr.override p.xattr p'.xattr;
+    acl = ACL.override p.acl p'.acl;
+  }
 
 let strip p =
   { perm = Perm.strip p.perm;
@@ -712,29 +1410,37 @@ let strip p =
     gid = Gid.strip p.gid;
     time = Time.strip p.time;
     typeCreator = TypeCreator.strip p.typeCreator;
-    length = p.length }
+    length = p.length;
+    ctime = p.ctime;
+    xattr = Xattr.strip p.xattr;
+    acl = ACL.strip p.acl;
+  }
 
 let toString p =
   Printf.sprintf
-    "modified on %s  size %-9.0f %s%s%s%s"
+    "modified on %s  size %-9.0f %s%s%s%s%s%s"
     (Time.toString p.time)
     (Uutil.Filesize.toFloat p.length)
     (Perm.toString p.perm)
     (Uid.toString p.uid)
     (Gid.toString p.gid)
+    (Xattr.toString p.xattr)
     (TypeCreator.toString p.typeCreator)
+    (ACL.toString p.acl)
 
 let syncedPartsToString p =
   let tm = Time.syncedPartsToString p.time in
   Printf.sprintf
-    "%s%s  size %-9.0f %s%s%s%s"
+    "%s%s  size %-9.0f %s%s%s%s%s%s"
     (if tm = "" then "" else "modified at ")
     tm
     (Uutil.Filesize.toFloat p.length)
     (Perm.syncedPartsToString p.perm)
     (Uid.syncedPartsToString p.uid)
     (Gid.syncedPartsToString p.gid)
+    (Xattr.syncedPartsToString p.xattr)
     (TypeCreator.syncedPartsToString p.typeCreator)
+    (ACL.syncedPartsToString p.acl)
 
 let diff p p' =
   { perm = Perm.diff p.perm p'.perm;
@@ -742,49 +1448,94 @@ let diff p p' =
     gid = Gid.diff p.gid p'.gid;
     time = Time.diff p.time p'.time;
     typeCreator = TypeCreator.diff p.typeCreator p'.typeCreator;
-    length = p'.length }
-
-let get stats infos =
-  { perm = Perm.get stats infos;
-    uid = Uid.get stats infos;
-    gid = Gid.get stats infos;
-    time = Time.get stats infos;
-    typeCreator = TypeCreator.get stats infos;
+    length = p'.length;
+    ctime = p'.ctime;
+    xattr = Xattr.diff p.xattr p'.xattr;
+    acl = ACL.diff p.acl p'.acl;
+  }
+
+let get' stats =
+  { perm = Perm.get stats;
+    uid = Uid.get stats;
+    gid = Gid.get stats;
+    time = Time.get stats;
+    typeCreator = TypeCreator.dummy;
     length =
       if stats.Unix.LargeFile.st_kind = Unix.S_REG then
         Uutil.Filesize.fromStats stats
       else
-        Uutil.Filesize.zero }
+        Uutil.Filesize.zero;
+    ctime = CTime.dummy;
+    xattr = Xattr.dummy;
+    acl = ACL.dummy;
+  }
+
+(* Important note about [fspath] and [path] arguments to [get]:
+   If the path points to a symlink then the [stats] argument may be the
+   result of either stat(2) or lstat(2) on said path. When this distinction
+   is important then it can be easily checked by seeing if [stats.st_kind]
+   is S_LNK or not. If it is not S_LNK then any syscalls/functions on this
+   path are expected to follow symlinks (and not follow otherwise). *)
+let get ?(archProps = dummy) fspath path stats infos =
+  let abspath = Fspath.concat fspath path in
+  (* Note for future: ctime could very well be included in [get'] but it
+     does not seem necessary at the moment. See the comment at
+     [CTime.override]. *)
+  let ctime = CTime.get stats in
+  let ctimeChanged = not (CTime.same_time ctime archProps.ctime) in
+  let props = get' stats in
+  { props with
+    typeCreator = TypeCreator.get stats infos;
+    ctime;
+    xattr =
+      if ctimeChanged || not Xattr.ctimeDetect then Xattr.get abspath stats
+      else archProps.xattr;
+    acl =
+      if ctimeChanged then ACL.get abspath stats
+      else archProps.acl;
+  }
+
+let getWithRess stats osXinfo =
+  let props = get' stats in
+  { props with
+    typeCreator = TypeCreator.get stats osXinfo;
+  }
 
 let set fspath path kind p =
-  Uid.set fspath path kind p.uid;
-  Gid.set fspath path kind p.gid;
-  TypeCreator.set fspath path kind p.typeCreator;
-  Time.set fspath path kind p.time;
-  Perm.set fspath path kind p.perm
+  let abspath = Fspath.concat fspath path in
+  Uid.set abspath p.uid;
+  Gid.set abspath p.gid;
+  TypeCreator.set fspath path p.typeCreator;
+  Xattr.set abspath p.xattr;
+  Time.set abspath p.time;
+  Perm.set abspath kind p.perm;
+  (* ACLs must always be set after chmod,
+   * otherwise chmod may replace the ACL. *)
+  ACL.set abspath p.acl
 
 (* Paranoid checks *)
 let check fspath path stats p =
+  ACL.check fspath path stats p.acl;
+  Xattr.check fspath path stats p.xattr;
   Time.check fspath path stats p.time;
   Perm.check fspath path stats p.perm
 
 let init someHostIsRunningWindows =
   Perm.init someHostIsRunningWindows;
   Uid.init someHostIsRunningWindows;
-  Gid.init someHostIsRunningWindows;
-  Time.init someHostIsRunningWindows;
-  TypeCreator.init someHostIsRunningWindows
+  Gid.init someHostIsRunningWindows
 
 let fileDefault = template Perm.fileDefault
 let fileSafe = template Perm.fileSafe
 let dirDefault = template Perm.dirDefault
 
 let same_time p p' = Time.same p.time p'.time
+let same_ctime p p' = CTime.same_time p.ctime p'.ctime
 let length p = p.length
 let setLength p l = {p with length=l}
 
 let time p = Time.extract p.time
-let setTime p t = {p with time = Time.replace p.time t}
+let setTime p p' = {p with time = Time.replace p.time (time p'); ctime = p'.ctime}
 
 let perms p = Perm.extract p.perm
 
@@ -794,6 +1545,72 @@ let dontChmod = Perm.dontChmod
 
 let validatePrefs = Perm.validatePrefs
 
+let loadExtData fspath path p =
+  let abspath = Fspath.concat fspath path in
+  { p with
+    xattr = Xattr.readAll abspath p.xattr;
+  }
+
+let purgeExtData p =
+  { p with
+    xattr = Xattr.purge p.xattr;
+  }
+
+let withExtData p =
+  { p with
+    xattr = Xattr.getAll p.xattr;
+  }
+
+(* ------------------------------------------------------------------------- *)
+(*                          Shared data for props                            *)
+(* ------------------------------------------------------------------------- *)
+
+module Data = struct
+
+  type e = string * (string * string) list
+  type d = e list
+
+  let m = Umarshal.(list (prod2 string (list (prod2 string string id id)) id id))
+
+  let enabled () =
+    xattrEnabled () || aclEnabled ()
+
+  let extract k pd = try Safelist.assoc k pd with Not_found -> []
+
+  let extern kind =
+    let add_nonempty k v pd =
+      match v with
+      | [] -> pd
+      | _ -> (k, v) :: pd
+    in
+    []
+    |> add_nonempty "xattr" (Xattr.Data.get kind)
+    |> add_nonempty "ACL" (ACL.Data.get kind)
+
+  let intern pd =
+    Xattr.Data.set (extract "xattr" pd);
+    ACL.Data.set (extract "ACL" pd);
+    ()
+
+  let merge pd =
+    Xattr.Data.merge (extract "xattr" pd);
+    ACL.Data.merge (extract "ACL" pd);
+    ()
+
+  let gcInit () =
+    Xattr.Data.clear `Kept;
+    ACL.Data.clear `Kept;
+    ()
+
+  let gcKeep p =
+    (* Xattr data cache is not persisted *)
+    ACL.Data.keep p.acl;
+    ()
+
+  let gcDone () = extern `Kept
+
+end
+
 (* ------------------------------------------------------------------------- *)
 (*                          Directory change stamps                          *)
 (* ------------------------------------------------------------------------- *)
@@ -803,6 +1620,8 @@ let validatePrefs = Perm.validatePrefs
 
 type dirChangedStamp = Uutil.Filesize.t
 
+let mdirChangedStamp = Uutil.Filesize.m
+
 let freshDirStamp () =
   let t =
     (Unix.gettimeofday () +. sqrt 2. *. float (Unix.getpid ())) *. 1000.
diff --git a/src/props.mli b/src/props.mli
index e881c39..636d8eb 100644
--- a/src/props.mli
+++ b/src/props.mli
@@ -3,41 +3,86 @@
 
 (* File properties: time, permission, length, etc. *)
 
-type t
-val dummy : t
+type t251
+type _ props
+type basic = [`Basic] props
+type t = [`Full] props
+type x = [`ExtLoaded] props
+val m : t Umarshal.t
+val mbasic : basic Umarshal.t
+val mx : x Umarshal.t
+val to_compat251 : _ props -> t251
+val of_compat251 : t251 -> _ props
+val dummy : _ props
 val hash : t -> int -> int
+val hash251 : t251 -> int -> int
 val similar : t -> t -> bool
-val override : t -> t -> t
+val override : _ props -> 'a props -> 'a props
 val strip : t -> t
-val diff : t -> t -> t
+val diff : t -> x -> x
 val toString : t -> string
 val syncedPartsToString : t -> string
-val set : Fspath.t -> Path.local -> [`Set | `Update] -> t -> unit
-val get : Unix.LargeFile.stats -> Osx.info -> t
-val check : Fspath.t -> Path.local -> Unix.LargeFile.stats -> t -> unit
+val set : Fspath.t -> Path.local -> [`Set | `Update] -> x -> unit
+val get' : Unix.LargeFile.stats -> basic
+val get : ?archProps:t -> Fspath.t -> Path.local -> Unix.LargeFile.stats -> Osx.info -> t
+val getWithRess : Unix.LargeFile.stats -> Osx.info -> basic
+val check : Fspath.t -> Path.local -> Unix.LargeFile.stats -> x -> unit
 val init : bool -> unit
 
-val same_time : t -> t -> bool
-val length : t -> Uutil.Filesize.t
+val loadExtData : Fspath.t -> Path.local -> t -> x
+val purgeExtData : x -> t
+val withExtData : t -> x
+(* [withExtData] will raise Not_found if some ext data is missing. In that
+   case, [loadExtData] must be used to load any missing ext data. *)
+
+val same_time : _ props -> t -> bool
+val same_ctime : _ props -> t -> bool
+val length : _ props -> Uutil.Filesize.t
 val setLength : t -> Uutil.Filesize.t -> t
-val time : t -> float
-val setTime : t -> float -> t
-val perms : t -> int
+val time : _ props -> float
+val setTime : t -> _ props -> t
+val perms : _ props -> int
 
-val fileDefault : t
+val fileDefault : basic
 val fileSafe : t
-val dirDefault : t
+val dirDefault : basic
 
 val syncModtimes : bool Prefs.t
 val permMask : int Prefs.t
 val dontChmod : bool Prefs.t
+val syncXattrs : bool Prefs.t
+val syncACL : bool Prefs.t
+
+val aclEnabled : unit -> bool
+val xattrEnabled : unit -> bool
+
+val xattrIgnorePred : Pred.t
+val xattrIgnorenotPred : Pred.t
 
 (* We are reusing the directory length to store a flag indicating that
    the directory is unchanged *)
 type dirChangedStamp
+val mdirChangedStamp : dirChangedStamp Umarshal.t
 val freshDirStamp : unit -> dirChangedStamp
 val changedDirStamp : dirChangedStamp
 val setDirChangeFlag : t -> dirChangedStamp -> int -> t * bool
 val dirMarkedUnchanged : t -> dirChangedStamp -> int -> bool
 
 val validatePrefs: unit -> unit
+
+module Data : sig
+  type e
+  type d = e list
+
+  val m : d Umarshal.t
+
+  val enabled : unit -> bool
+
+  val extern : [`New] -> d
+  val intern : d -> unit
+  val merge : d -> unit
+
+  val gcInit : unit -> unit
+  val gcKeep : t -> unit
+  val gcDone : unit -> d
+end
diff --git a/src/props_acl.c b/src/props_acl.c
new file mode 100644
index 0000000..b3a3bba
--- /dev/null
+++ b/src/props_acl.c
@@ -0,0 +1,821 @@
+/* Unison file synchronizer: src/props_acl.c */
+/* Copyright 2020-2022, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* Supporting POSIX draft ACLs is not a goal, but may incidentally work
+ * on some platforms. Only NFSv4 ACLs and Windows ACLs are intended to be
+ * supported.
+ *
+ * On Solarish, both NFSv4 ACLs and POSIX draft ACLs are supported.
+ * There is even support for cross-synchronizing between NFSv4 and
+ * POSIX draft ACLs, but this support is currently disabled in props.ml
+ * by checking if the resulting ACL matches the requested ACL (the check
+ * fails with cross-synchronization).
+ *
+ * On FreeBSD and NetBSD, NFSv4 ACLs are supported. There is only limited
+ * support for synchronizing POSIX draft ACLs (no default ACLs).
+ *
+ * On Darwin, extended ACLs are supported.
+ *
+ * On Windows, NTFS ACLs are supported via SDDL format. Only explicit
+ * ACEs are synchronized, ignoring inherited ACEs completely. Users and
+ * groups are represented as SID strings in SDDL, not as names.
+ */
+
+/* The external interface is defined as follows. Every supported platform
+ * must implement this interface. ACL format can be platform-specific,
+ * which will prevent cross-platform synchronization but still allows
+ * synchronization within the platform.
+ *
+ *
+ * SET the ACL
+ * ===========
+ * unit unison_acl_from_text(String path, String acl)
+ *
+ *   Set the requested ACL on the requested file or directory. The ACL
+ *   must be in the same format as that returned by unison_acl_to_text().
+ *   Empty string ACL means <no ACL> and results in removal of any
+ *   existing ACL on the requested file or directory.
+ *   Symbolic links are followed.
+ *
+ * Input parameters
+ *   path - absolute path of a file or directory
+ *   acl  - text representation of ACL to set on the path
+ *
+ * Return value
+ *   No return value.
+ *
+ * Exceptions
+ *   There are no mandatory exception conditions.
+ *   Failure MAY voluntarily be raised for example when:
+ *     Can't access file to set/remove ACL
+ *     ACL not supported
+ *     Error setting ACL
+ *     Error removing ACL
+ *     Error converting ACL from text
+ *
+ *
+ * GET the ACL
+ * ===========
+ * String unison_acl_to_text(String path)
+ *
+ *   Get the current ACL on the requested file or directory. The ACL
+ *   must be returned as a stable and deterministic text representation
+ *   that meets the following criteria:
+ *     - with multiple requests on the same file, the representation is
+ *       always the same, unless the underlying ACL changes;
+ *     - the same ACL on different files has the same representation.
+ *   Symbolic links are followed.
+ *
+ * Input parameters
+ *   path - absolute path of a file or directory
+ *
+ * Return value
+ *   The text representation of the ACL;
+ *   or the value of macro UNSN_ACL_EMPTY (or empty string "") meaning
+ *     <no ACL> (or only trivial ACL)
+ *   or the value of macro UNSN_ACL_NOT_SUPPORTED (currently "-1") if
+ *     ACL is not supported on the requested path.
+ *
+ * Exceptions
+ *   Failure MUST be raised when:
+ *     Can't access file to get ACL
+ *   Failure MAY voluntarily be raised for example when:
+ *     Error getting ACL
+ *     Error converting ACL to text
+ *   If Failure is not raised on some error condition then an empty
+ *   string "" MUST NOT be returned under any circumstances; return
+ *   UNSN_ACL_NOT_SUPPORTED instead.
+ *
+ *
+ * ===========
+ * Definition of ACL format
+ *
+ * The format of ACL text representation is completely free as long as
+ * following constraints are met:
+ *   - output of unison_acl_to_text() can be used as
+ *     input to unison_acl_from_text()
+ *   - ACL synchronization is done only on the same platform.
+ *
+ * If ACLs must be synchronized between different platforms then the
+ * currently used universal ACL format matches the definition from
+ * illumos acl(5) man page [https://illumos.org/man/5/acl]. This applies
+ * to both POSIX draft ACLs and NFSv4 ACLs. See the note on cross-platform
+ * synchronization below.
+ *
+ * ACL is always in the form
+ *
+ *   acl_entry[,acl_entry]...
+ *
+ * Each acl_entry may be suffixed with a colon and userid/groupid.
+ *
+ * Examples:
+ *
+ *   POSIX draft ACL
+ *
+ *     user:tom:rw-,mask:rwx,group:staff:r-x:450
+ *
+ *   NFSv4 ACL
+ *
+ *        user:lp:rw------------:------I:allow:1300,
+ *         owner@:--x-----------:------I:deny,
+ *         owner@:rw-p---A-W-Co-:-------:allow,
+ *     user:marks:r-------------:------I:deny:1270,
+ *         group@:r-------------:-------:allow,
+ *      everyone@:r-----a-R-c--s:-------:allow
+ *
+ *     (note that the example is folded, but it should actually be
+ *     returned as one string line without newlines)
+ *
+ *
+ * ===========
+ * On cross-platform synchronization
+ *
+ * Currently there is no canonical ACL representation created specifically
+ * for Unison. Existing platform APIs are used as much as possible, without
+ * custom formatting and parsing.
+ * A specific Unison ACL format could be truly common across platforms.
+ *
+ * If extended ACL synchronization capability is desired in the future then
+ * it is only required to change the output of unison_acl_to_text() and the
+ * input parsing in unison_acl_from_text().
+ * The Unison archive format will not have to be changed as long as the
+ * entire ACL and any eventual metadata is encoded within one string.
+ * It is neither necessary to change the ACL code in props.ml.
+ *
+ * The issues of such cross-platform (e.g. between Windows and Unix-like)
+ * synchronization lie not in the representation format, though. It is easy
+ * enough to interpret the permission sets of NFSv4 and Windows ACLs in a
+ * similar, equivalent way. It can be much more difficult to interpret the
+ * subjects (users and groups) in a meaningful way. Purely for
+ * synchronization, this can still work on some platforms, e.g. Solaris,
+ * which allow the use of SIDs in ACL definition.
+ */
+
+#include <caml/memory.h>
+#include <caml/alloc.h>
+#include <caml/mlvalues.h>
+#include <caml/fail.h>
+
+
+#if defined(sun) || defined(__sun)  /* Solarish, all illumos-based OS,   */
+#define __Solaris__                 /* OpenIndiana, OmniOS, SmartOS, ... */
+#endif
+
+/* Primitive check only, without explicitly checking for
+ * POSIX or NFSv4. NFSv4-style ACLs are expected
+ * but POSIX draft ACLs may work to some extent. */
+#undef UNSN_HAS_FS_ACL
+#if defined(__Solaris__) || defined(__FreeBSD__) || defined(__APPLE__)
+#define UNSN_HAS_FS_ACL
+#endif
+
+#if defined(__NetBSD__)
+#include <unistd.h>
+#if defined(_PC_ACL_NFS4)
+#define UNSN_HAS_FS_ACL
+#endif
+#endif
+
+#if defined(_WIN32)
+#define UNSN_HAS_FS_ACL
+#endif
+
+
+#define UNSN_ACL_NOT_SUPPORTED caml_copy_string("-1")
+
+
+#ifndef UNSN_HAS_FS_ACL
+
+CAMLprim value unison_acl_from_text(value path, value acl)
+{
+  CAMLparam0();
+  CAMLreturn(Val_unit);
+}
+
+CAMLprim value unison_acl_to_text(value path)
+{
+  CAMLparam0();
+  CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
+}
+
+#else
+
+
+#define UNSN_ACL_EMPTY caml_copy_string("")
+
+
+#if defined(_WIN32)
+
+/*#define ACL_DEBUG*/
+
+#ifndef UNICODE
+#define UNICODE
+#endif
+#ifndef _UNICODE
+#define _UNICODE
+#endif
+
+#include <windows.h>
+#include <aclapi.h>
+#include <sddl.h>
+#include <strsafe.h>
+
+#include <caml/version.h>
+#if OCAML_VERSION < 41300
+#define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */
+#endif
+#include <caml/osdeps.h>
+
+#ifdef ACL_DEBUG
+#include <stdio.h>
+#endif
+
+static void unsn_acl_fail(char *msg, DWORD err)
+{
+  DWORD flags;
+  char *sys_msg;
+  DWORD sys_len;
+  char fail_msg[160];
+  const size_t LEN = sizeof(fail_msg) / sizeof(fail_msg[0]);
+
+  flags =
+    FORMAT_MESSAGE_ALLOCATE_BUFFER |
+    FORMAT_MESSAGE_FROM_SYSTEM |
+    FORMAT_MESSAGE_IGNORE_INSERTS;
+
+  sys_len = FormatMessageA(flags, NULL, err, 0, (char *) &sys_msg, 0, NULL);
+  if (!sys_len) {
+    StringCbPrintfA(fail_msg, LEN, "%s (Windows error code: %d)", msg, err);
+  } else {
+    /* Assume last 3 characters are ".\r\n" (doesn't matter if they aren't),
+     * and remove them. */
+    if (sys_len > 3) {
+      sys_msg[sys_len - 3] = '\0';
+    }
+
+    StringCbPrintfA(fail_msg, LEN,
+      "%s (Windows error code: %d) %s", msg, err, sys_msg);
+    LocalFree(sys_msg);
+  }
+
+  caml_failwith(fail_msg);
+}
+
+CAMLprim value unison_acl_from_text(value path, value acl)
+{
+  CAMLparam2(path, acl);
+  wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
+  wchar_t *wacl = caml_stat_strdup_to_utf16(String_val(acl));
+  PCWSTR acl_text;
+  PSECURITY_DESCRIPTOR sd;
+  SECURITY_DESCRIPTOR_CONTROL sdc;
+  DWORD sdc_rev;
+  PSID owner = NULL, group = NULL;
+  PACL DACL;
+  BOOL DACLpresent = FALSE, isDef;
+  BOOL ok = TRUE;
+  SECURITY_INFORMATION si = 0;
+  DWORD res;
+
+#ifdef ACL_DEBUG
+  printf_s(" ===> Setting ACL for |%ls|\n", wpath);
+  printf_s(" ---> Input ACL value   |%ls|\n", wacl);
+#endif
+
+  if (wcslen(wacl) == 0) {
+    acl_text = L"D:"; /* SDDL representation of empty ACL */
+  } else {
+    acl_text = wacl;
+  }
+#ifdef ACL_DEBUG
+  printf_s(" ---> Setting ACL value |%ls|\n", acl_text);
+#endif
+
+  if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(acl_text,
+         SDDL_REVISION_1, &sd, NULL)) {
+    caml_stat_free(wpath);
+    caml_stat_free(wacl);
+    unsn_acl_fail("Error converting ACL from text", GetLastError());
+  }
+
+  caml_stat_free(wacl);
+
+  ok = ok && GetSecurityDescriptorDacl(sd, &DACLpresent, &DACL, &isDef);
+  ok = ok && GetSecurityDescriptorControl(sd, &sdc, &sdc_rev);
+
+  if (!ok || !DACLpresent) {
+    LocalFree(sd);
+    caml_stat_free(wpath);
+
+    caml_failwith("Error converting ACL from text (no ACL info present?)");
+  }
+
+  si |= DACL_SECURITY_INFORMATION;
+
+  if (sdc & SE_DACL_PROTECTED) {
+    si |= PROTECTED_DACL_SECURITY_INFORMATION;
+  } else {
+    si |= UNPROTECTED_DACL_SECURITY_INFORMATION;
+  }
+
+  res = SetNamedSecurityInfoW(wpath, SE_FILE_OBJECT,
+          si, owner, group, DACL, NULL);
+
+  LocalFree(sd);
+  caml_stat_free(wpath);
+
+  if (res == ERROR_ACCESS_DENIED) {
+    caml_failwith("Error setting ACL: access denied. The process may require "
+      "Administrator or \"Restore files\" privileges to set the ACL");
+  }
+
+  if (res != ERROR_SUCCESS) {
+    unsn_acl_fail("Error setting ACL", res);
+  }
+
+  CAMLreturn(Val_unit);
+}
+
+CAMLprim value unison_acl_to_text(value path)
+{
+  CAMLparam1(path);
+  CAMLlocal1(result);
+  wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
+  int i, aceCnt;
+  PWSTR acl_text;
+  PSECURITY_DESCRIPTOR sd;
+  SECURITY_DESCRIPTOR_CONTROL sdc;
+  DWORD sdc_rev;
+  PSID owner, group;
+  PACL DACL;
+  ACL_SIZE_INFORMATION aclInfo;
+  PVOID ace;
+  SECURITY_INFORMATION si = DACL_SECURITY_INFORMATION;
+  DWORD res1, err;
+  BOOL res2;
+
+#ifdef ACL_DEBUG
+  printf_s(" ===> Getting ACL for %ls\n", wpath);
+#endif
+
+  res1 = GetNamedSecurityInfoW(wpath, SE_FILE_OBJECT, si,
+           &owner, &group, &DACL, NULL, &sd);
+  caml_stat_free(wpath);
+
+  if (res1 != ERROR_SUCCESS || sd == NULL) {
+    unsn_acl_fail("Error getting ACL", res1);
+  }
+
+#ifdef ACL_DEBUG
+  res2 = ConvertSecurityDescriptorToStringSecurityDescriptorW(sd,
+           SDDL_REVISION_1, si, &acl_text, NULL);
+
+  if (acl_text != NULL) {
+    printf_s(" ---> Initial ACL text representation: %ls\n", acl_text);
+
+    LocalFree(acl_text);
+  }
+#endif /* ACL_DEBUG */
+
+  if (DACL == NULL) {
+    LocalFree(sd);
+
+#ifdef ACL_DEBUG
+    printf_s(" ---> ACL not supported\n");
+#endif
+    CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
+  }
+
+  if (!GetAclInformation(DACL, &aclInfo, sizeof(aclInfo), AclSizeInformation)) {
+    LocalFree(sd);
+    unsn_acl_fail("Error getting ACL information", GetLastError());
+  }
+  aceCnt = aclInfo.AceCount;
+
+  /* Remove all inherited ACEs -- those cannot be restored in the other
+   * replica, they are inherited from the parent directory. */
+  for (i = aclInfo.AceCount - 1; i >= 0; i--) {
+    if (!GetAce(DACL, i, &ace)) {
+#ifdef ACL_DEBUG
+      printf_s("GetAce failed (Windows error code %d)\n", GetLastError());
+#endif
+    } else if (((PACE_HEADER) ace)->AceFlags & INHERITED_ACE) {
+      if (!DeleteAce(DACL, i)) {
+#ifdef ACL_DEBUG
+        printf_s("DeleteAce failed (Windows error code %d)\n", GetLastError());
+#endif
+      } else {
+        aceCnt--;
+      }
+    }
+  }
+
+  /* Even when individual inherited ACEs have been removed, the entire ACL
+   * may have been marked as AUTO_INHERITED. Remove this flag to make
+   * synchronization paranoid checks more reliable. It is unknown if it
+   * can cause synchronization failures, but it doesn't matter - inherited
+   * ACLs can't be propagated in any case. */
+  if (!SetSecurityDescriptorControl(sd, SE_DACL_AUTO_INHERITED, 0)) {
+#ifdef ACL_DEBUG
+    unsn_acl_fail("Error in ACL control information", GetLastError());
+#endif
+  }
+
+  if (aceCnt == 0) { /* No explicit entries */
+    if (!GetSecurityDescriptorControl(sd, &sdc, &sdc_rev)) {
+      LocalFree(sd);
+      unsn_acl_fail("Error getting ACL control information", GetLastError());
+    }
+
+    if (!(sdc & SE_DACL_PROTECTED)) { /* No control flags we care about */
+      LocalFree(sd);
+
+#ifdef ACL_DEBUG
+      printf_s(" ---> Empty ACL (no explicit ACE, may have inherited ACE)\n");
+#endif
+      CAMLreturn(UNSN_ACL_EMPTY); /* Empty ACL (or only inherited) */
+    }
+  }
+
+  res2 = ConvertSecurityDescriptorToStringSecurityDescriptor(sd,
+           SDDL_REVISION_1, si, &acl_text, NULL);
+  err = GetLastError();
+
+  LocalFree(sd);
+
+  if (!res2 || (acl_text == NULL)) {
+    unsn_acl_fail("Error converting ACL to text", err);
+  }
+
+#ifdef ACL_DEBUG
+  printf_s(" ---> Final ACL text representation:   %ls\n", acl_text);
+#endif
+
+  result = caml_copy_string_of_utf16(acl_text);
+
+  LocalFree(acl_text);
+
+  CAMLreturn(result);
+}
+
+
+#else /* defined(_WIN32) */
+
+
+#if defined(__Solaris__) || defined(__APPLE__)
+#include <fcntl.h>
+#include <sys/stat.h>
+#endif
+
+#if defined(__Solaris__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/acl.h>
+#endif
+
+#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+#include <unistd.h>
+#endif
+
+#if defined(__APPLE__)
+#define UNSN_ACL_T acl_t
+#else
+#define UNSN_ACL_T acl_t *
+#endif
+
+
+static void unsn_acl_fail(const char *fmtmsg)
+{
+  char errmsg[255];
+
+  int errnum = errno;
+  if (strerror_r(errnum, errmsg, sizeof(errmsg)) != 0) {
+    snprintf(errmsg, sizeof(errmsg), "(error code %d)", errnum);
+  }
+
+  caml_failwith_value(caml_alloc_sprintf(fmtmsg, errmsg));
+}
+
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+static acl_type_t unsn_path_acl_type(const char *path)
+{
+  if (pathconf(path, _PC_ACL_NFS4) > 0) { /* NFSv4 ACL supported */
+    return ACL_TYPE_NFS4;
+  } else if (pathconf(path, _PC_ACL_EXTENDED) > 0) { /* POSIX draft ACL */
+    return ACL_TYPE_ACCESS; /* It is not possible to get or set
+                               default and access ACL at the same time,
+                               so fall back to access ACL only. */
+  } else { /* ACLs not supported */
+    return -1;
+  }
+}
+#elif defined(__APPLE__)
+static acl_type_t unsn_path_acl_type(const char *path)
+{
+  return ACL_TYPE_EXTENDED;
+}
+#endif
+
+
+static void unsn_remove_acl_os(const char *path)
+{
+#if defined(__Solaris__)
+  struct stat st;
+
+  if (stat(path, &st) != 0) {
+    unsn_acl_fail("Can't access file to remove ACL: %s");
+  }
+
+  if (acl_strip(path, st.st_uid, st.st_gid, st.st_mode) != 0) {
+    unsn_acl_fail("Error removing ACL: %s");
+  }
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  /* FreeBSD has a acl_strip_np() function, but it would be
+   * much too complicated in this code. */
+  /* Don't even bother checking for target ACL type, just
+   * try to remove all and ignore errors. */
+  acl_delete_file_np(path, ACL_TYPE_DEFAULT);
+  acl_delete_file_np(path, ACL_TYPE_ACCESS);
+  acl_delete_file_np(path, ACL_TYPE_NFS4);
+#elif defined(__APPLE__)
+  acl_set_file(path, unsn_path_acl_type(path), acl_from_text("!#acl 1"));
+#endif
+}
+
+
+/************************************
+ *         Set ACL from text
+ ************************************/
+static _Bool unsn_acl_from_text_os(const char *acl_text, UNSN_ACL_T *aclp)
+{
+#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+  *aclp = acl_from_text(acl_text);
+
+  return (*aclp != NULL);
+#elif defined(__Solaris__)
+  int error = acl_fromtext(acl_text, aclp);
+
+  return (error == 0 && aclp != NULL);
+#endif
+}
+
+CAMLprim value unison_acl_from_text(value path, value acl)
+{
+  CAMLparam2(path, acl);
+  const char *acl_text = String_val(acl);
+  const char *name = String_val(path);
+  UNSN_ACL_T aclp = NULL;
+  int error;
+
+#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+  acl_type_t type = unsn_path_acl_type(name);
+  if (type == -1) {
+    caml_failwith("ACL not supported on this path");
+  }
+#endif
+
+  /* Check if ACL must be removed */
+  if (*acl_text == '\0') {
+    unsn_remove_acl_os(name);
+    CAMLreturn(Val_unit);
+  }
+
+  if (!unsn_acl_from_text_os(acl_text, &aclp)) {
+    caml_failwith("Error converting ACL from text");
+  }
+
+#if defined(__Solaris__)
+  error = acl_set(name, aclp);
+#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+  error = acl_set_file(name, type, aclp);
+#endif
+  int real_err = errno;
+  acl_free(aclp);
+  errno = real_err;
+
+  if (error == -1) {
+    unsn_acl_fail("Error setting ACL: %s");
+  }
+
+  CAMLreturn(Val_unit);
+}
+
+
+/************************************
+ *          Get ACL as text
+ ************************************/
+/* This function does not allocate new,
+ * it returns the pointer to its argument. */
+static char *postprocess_acl_os(char *s)
+{
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+  char *p;
+  char *buf = s;  /* Just an alias; modify input string in place */
+  int perms = 0, comment = 0, offs = 0;
+
+  for (p = s; *p; p++) {
+    switch (*p) {
+      case ',' :
+          perms = 0;
+          break;
+      case '#' :
+          /* FreeBSD acl_to_text embeds the #effective permissions,
+           * which are actually not part of the ACL. */
+          comment = 1;
+          break;
+      case '@' :
+      case ':' :
+          if (!comment) {
+            perms++;
+          }
+          break;
+      case 'D' :
+          /* Swap the position of d and D permissions.
+           * Synchronization works even without swapping, but the different
+           * ordering will show up as constant synchronization difference. */
+          if (perms == 2) {
+            if (buf[offs - 1] != 'd' && *(p + 1) == 'd') {
+              *p = 'd';
+              *(p + 1) = 'D';
+            } else if (buf[offs - 1] != 'd' && *(p + 1) == '-') {
+              *p = '-';
+              *(p + 1) = 'D';
+            }
+            perms = 0; /* prevent further swapping */
+          }
+          break;
+      case 'd' :
+          if (perms == 2) {
+            if (buf[offs - 1] == '-' && *(p + 1) != 'D' && *(p + 1) != '\0') {
+              buf[offs - 1] = 'd';
+              *p = '-';
+            } else if (buf[offs - 1] != 'd' && *(p + 1) == '-') {
+              *p = '-';
+              *(p + 1) = 'D';
+            }
+            perms = 0; /* prevent further swapping */
+          }
+          break;
+      case '\n' :
+          /* Replace newlines with commas...
+           * ... except if it's the last one. */
+          if (*(p + 1) != '\0') {
+            *p = ',';
+          } else {
+            *p = ' ';
+          }
+          perms = 0;
+          comment = 0;
+          break;
+    }
+
+    /* Remove all whitespace and comments. */
+    if (*p != ' ' && *p != '\t' && !comment) {
+      buf[offs++] = *p;
+    }
+  }
+  buf[offs] = '\0';
+
+  return buf;
+#elif defined(__APPLE__)
+  /* Remove trailing newline */
+  size_t last = strlen(s) - 1;
+  if (last >= 0 && s[last] == '\n') {
+    s[last] = '\0';
+  }
+
+  return s;
+#endif
+}
+
+static char *unsn_acl_to_text_os(UNSN_ACL_T aclp)
+{
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+  return postprocess_acl_os(acl_to_text_np(aclp, NULL, ACL_TEXT_APPEND_ID));
+#elif defined(__APPLE__)
+  return postprocess_acl_os(acl_to_text(aclp, NULL));
+#elif defined(__Solaris__)
+  return acl_totext(aclp, ACL_APPEND_ID | ACL_COMPACT_FMT | ACL_SID_FMT);
+#endif
+}
+
+static _Bool unsn_acl_is_empty_or_trivial_os(UNSN_ACL_T aclp)
+{
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+  int is_trivial = 0;
+
+  acl_is_trivial_np(aclp, &is_trivial); /* Ignore any errors here */
+
+  return (is_trivial || aclp == NULL);
+#elif defined(__APPLE__) || defined(__Solaris__)
+  return (aclp == NULL);
+#endif
+}
+
+static int unsn_get_acl_os(const char *path, UNSN_ACL_T *aclp)
+{
+#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__APPLE__)
+  acl_type_t type = unsn_path_acl_type(path);
+  if (type == -1) {
+    errno = EOPNOTSUPP;
+    return -1;
+  }
+
+  errno = 0;
+  *aclp = acl_get_file(path, type);
+
+#if defined(__APPLE__)
+  if (errno != 0) {
+    /* ACLs are always enabled on Darwin since version 10 (2009).
+     * Unfortunately, Darwin sets errno to ENOENT also when the file
+     * does not have an extended ACL. Since it is impossible to distinguish
+     * from the real ENOENT (due to the path), we must check with stat(). */
+    if (errno == ENOENT) {
+      struct stat st;
+      errno = 0;
+      stat(path, &st);
+
+      if (errno != ENOENT) {
+        /* The path does not trigger ENOENT;
+         * this means that there is no extended ACL. This is allowed. */
+        return 0;
+      }
+
+      errno = ENOENT; /* Ignore errno from stat(), restore original errno. */
+    }
+
+    return -1;
+  }
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  if (*aclp == NULL) {
+    return -1;
+  }
+#endif
+
+  return 0;
+#endif  /* FreeBSD or NetBSD or Darwin */
+
+#if defined(__Solaris__)
+  return acl_get(path, ACL_NO_TRIVIAL, aclp);
+#endif
+}
+
+CAMLprim value unison_acl_to_text(value path)
+{
+  CAMLparam1(path);
+  CAMLlocal1(result);
+  UNSN_ACL_T aclp = NULL;
+  char *acltxt;
+
+  int err = unsn_get_acl_os(String_val(path), &aclp);
+  if (err == -1) {
+    if (errno == ENOSYS || errno == EOPNOTSUPP) {
+      CAMLreturn(UNSN_ACL_NOT_SUPPORTED);
+    } else {
+      unsn_acl_fail("Error getting ACL: %s");
+    }
+  }
+
+  /* If there was no error but aclp is NULL then it means an empty
+   * or trivial ACL (that is, just the mode), which is allowed. */
+  if (aclp == NULL || unsn_acl_is_empty_or_trivial_os(aclp)) {
+    if (aclp != NULL) {
+      acl_free(aclp);
+    }
+
+    CAMLreturn(UNSN_ACL_EMPTY);
+  }
+
+  acltxt = unsn_acl_to_text_os(aclp);
+  acl_free(aclp);
+
+  if (acltxt == NULL) {
+    caml_failwith("Error converting ACL to text");
+  }
+
+  result = caml_copy_string(acltxt);
+  free(acltxt);
+
+  CAMLreturn(result);
+}
+
+#endif /* !defined(_WIN32) */
+
+
+#endif /* UNSN_HAS_FS_ACL */
diff --git a/src/props_xattr.c b/src/props_xattr.c
new file mode 100644
index 0000000..d832481
--- /dev/null
+++ b/src/props_xattr.c
@@ -0,0 +1,770 @@
+/* Unison file synchronizer: src/props_xattr.c */
+/* Copyright 2020-2022, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/* Conceptually, here, an extended attribute is just a name-value pair,
+ * where name is a text string and value is a binary string. This matches
+ * well the concept of extended attributes on some platforms (notably,
+ * Linux and BSDs), while some platforms provide more sophisticated
+ * extended attributes.
+ *
+ * The external interface is defined as follows. Every supported platform
+ * must implement this interface. xattr format can be platform-specific,
+ * which may prevent cross-platform synchronization but still allows
+ * synchronization within the platform. Cross-platform synchronization may
+ * still be possible in some cases, even if one platform will not
+ * understand the xattrs; the attribute values are treated as blobs then.
+ *
+ *
+ * SET the value of one xattr
+ * ==========================
+ * unit unison_xattr_set(String path, String xattrname, String xattrvalue)
+ *
+ *   Create the requested extended attribute on the requested file or
+ *   directory and set the attribute value.
+ *   If the attribute already exists then its value is overwritten.
+ *   Symbolic links are followed.
+ *
+ * Input parameters
+ *   path       - absolute path of a file or directory
+ *   xattrname  - name of attribute to set on the path
+ *   xattrvalue - value of attribute to set on the path
+ *
+ * Return value
+ *   No return value.
+ *
+ * Exceptions
+ *   There are no mandatory exception conditions.
+ *   OCaml exception defined by macro UNSN_XATTR_NOT_SUPPORTED_EX
+ *   MAY be raised when extended attributes are not supported on
+ *   the requested path.
+ *   Failure MAY voluntarily be raised for example when:
+ *     Can't access file to set the attribute
+ *     Error creating the attribute (invalid name, permission error, etc.)
+ *     Error setting the attribute value
+ *
+ *
+ * REMOVE one xattr
+ * ================
+ * unit unison_xattr_remove(String path, String xattrname)
+ *
+ *   Remove the requested extended attribute on the requested file or
+ *   directory. Symbolic links are followed.
+ *
+ * Input parameters
+ *   path      - absolute path of a file or directory
+ *   xattrname - name of attribute to remove on the path
+ *
+ * Return value
+ *   No return value.
+ *
+ * Exceptions
+ *   There are no mandatory exception conditions.
+ *   OCaml exception defined by macro UNSN_XATTR_NOT_SUPPORTED_EX
+ *   MAY be raised when extended attributes are not supported on
+ *   the requested path.
+ *   Failure MAY voluntarily be raised for example when:
+ *     Can't access file to remove the attribute
+ *     Error removing the attribute
+ *
+ *
+ * GET the value of one xattr
+ * ==========================
+ * String unison_xattr_get(String path, String xattrname)
+ *
+ *   Get the value of the requested extended attribute on the requested
+ *   file or directory. The entire value is returned in full length.
+ *   Symbolic links are followed.
+ *
+ * Input parameters
+ *   path      - absolute path of a file or directory
+ *   xattrname - name of attribute to get on the path
+ *
+ * Return value
+ *   The value of the requested extended attribute, as a binary string.
+ *
+ * Exceptions
+ *   OCaml exception defined by macro UNSN_XATTR_NOT_SUPPORTED_EX
+ *   MAY be raised when extended attributes are not supported on
+ *   the requested path.
+ *   Failure MUST be raised when:
+ *     The attribute value does not fit in an OCaml string on a 32-bit
+ *     platform (approx. 16 MB)
+ *     Can't access file to read the attribute
+ *     Error reading the attribute value or attribute not found
+ *
+ *
+ * GET the list of xattrs with value lengths
+ * =========================================
+ * List of (String * Int) unison_xattrs_list(String path)
+ *
+ *   Get the list of all extended attributes on the requested file or
+ *   directory. Attributes names are returned together with the length
+ *   of attribute values.
+ *   Attributes in the list can be returned in any order and the order
+ *   does not have to be stable (i.e. it can be different on every
+ *   invocation on the same path).
+ *   Symbolic links are followed.
+ *
+ * Input parameters
+ *   path - absolute path of a file or directory
+ *
+ * Return value
+ *   The list of name-length pairs, with each pair representing the
+ *   name and length of value of one extended attribute.
+ *
+ * Exceptions
+ *   OCaml exception defined by macro UNSN_XATTR_NOT_SUPPORTED_EX
+ *   MUST be raised when extended attributes are not supported on
+ *   the requested path, or should not otherwise be returned.
+ *   Failure MAY voluntarily be raised for example when:
+ *     Can't access file to get the attributes
+ *     Error reading attribute values
+ *
+ *
+ * Indicate platform/system capabilities
+ * =====================================
+ * Boolean unison_xattr_updates_ctime()
+ *
+ *   Indicate whether the platform/system updates file ctime when
+ *   extended attributes change on the file. Not all platforms do this
+ *   (Solaris/illumos are known to not update any stat times of the
+ *   file/directory when its extended attributes are modified).
+ *   The capabilities of the system are important to know because
+ *   detecting updates quickly yet correctly relies on knowing when to
+ *   get the list of xattrs with values.
+ *
+ * Input parameters
+ *   none
+ *
+ * Return value
+ *   True if file ctime is updated at xattr changes. False otherwise.
+ *   For a platform that does not support ctime, true can be returned
+ *   if xattr changes update file mtime.
+ *
+ * Exceptions
+ *   none
+ *
+ */
+
+#include <caml/memory.h>
+#include <caml/alloc.h>
+#include <caml/fail.h>
+#include <caml/callback.h>
+
+
+#if defined(sun) || defined(__sun)  /* Solarish, all illumos-based OS,   */
+#define __Solaris__                 /* OpenIndiana, OmniOS, SmartOS, ... */
+#endif
+
+#undef UNSN_HAS_XATTR
+#if defined(__Solaris__) || defined(__FreeBSD__) || defined(__NetBSD__) \
+    || defined(__APPLE__) || defined(__linux)
+#define UNSN_HAS_XATTR
+#endif
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+
+#define UNSN_XATTR_NOT_SUPPORTED_EX "XattrNotSupported"
+
+
+static void unsn_xattr_not_supported()
+{
+  static const value *ex = NULL;
+
+  if (ex == NULL) {
+    ex = caml_named_value(UNSN_XATTR_NOT_SUPPORTED_EX);
+  }
+
+  caml_raise_constant(*ex);
+}
+
+
+#ifndef UNSN_HAS_XATTR
+
+CAMLprim void unison_xattr_set(value path, value xattrname, value xattr)
+{
+  unsn_xattr_not_supported();
+}
+
+CAMLprim void unison_xattr_remove(value path, value xattrname)
+{
+  unsn_xattr_not_supported();
+}
+
+CAMLprim void unison_xattr_get(value path, value xattrname)
+{
+  unsn_xattr_not_supported();
+}
+
+CAMLprim void unison_xattrs_list(value path)
+{
+  unsn_xattr_not_supported();
+}
+
+CAMLprim value unison_xattr_updates_ctime(value unit)
+{
+  CAMLparam0();
+  CAMLreturn(Val_true);
+}
+
+#else /* UNSN_HAS_XATTR */
+
+
+#if defined(__Solaris__)
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdio.h>
+#endif
+
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/extattr.h>
+#include <string.h>
+#include <stdio.h>
+#endif
+
+#if defined(__FreeBSD__)
+#define ENOTSUP EOPNOTSUPP
+#endif
+
+#if defined(__APPLE__)
+#include <errno.h>
+#include <sys/xattr.h>
+#include <string.h>
+#include <stdio.h>
+#endif
+
+#if defined(__linux)
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/xattr.h>
+#include <string.h>
+#include <stdio.h>
+
+/* Attribute names on Linux must be mangled to make cross-platform
+ * synchronization possible. When listing attributes, the "user."
+ * prefix is removed for user namespace attributes and an "!" is
+ * prepended to attribute names in all other namespaces (or more
+ * accurately, it is prepended to the namespace name).
+ *
+ * When feeding the attribute names to get, set, remove and other
+ * syscalls, the reverse is done. */
+
+#define XN_BUF_LEN 261
+#define XN_LEN (XN_BUF_LEN - 6)
+
+static value val_of_attrname(char *attrname)
+{
+  char buf[XN_BUF_LEN] = "!";
+
+  if (attrname != NULL) {
+    if (strncmp(attrname, "user.", 5) == 0) {
+      attrname += 5;
+    } else {
+      attrname = strncat(buf, attrname, XN_LEN);
+    }
+  }
+
+  return caml_copy_string(attrname != NULL ? attrname : "");
+}
+
+static const char *attrname_of_val(const char *attrname, char *buf)
+{
+  if (attrname != NULL) {
+    if (attrname[0] == '!') {
+      return attrname + 1;
+    } else {
+      return strncat(strcpy(buf, "user."), attrname, XN_LEN);
+    }
+  } else {
+    return attrname;
+  }
+}
+#endif /* defined(__linux) */
+
+
+#if defined(__linux)
+
+#define XATTRNAME_VAL(a, n) char xnb_[XN_BUF_LEN];\
+                            const char *a = attrname_of_val(String_val(n), xnb_)
+#define VAL_XATTRNAME val_of_attrname
+
+#else
+
+#define XATTRNAME_VAL(a, n) const char *a = String_val(n)
+#define VAL_XATTRNAME caml_copy_string
+
+#endif /* defined(__linux) */
+
+
+static void unsn_xattr_fail(const char *fmtmsg)
+{
+  char buf[512];
+  char *errmsg;
+
+#if defined(_WIN32)
+  errmsg = strerror(errno);
+#else
+  int errnum = errno;
+  if (strerror_r(errnum, buf, sizeof(buf)) != 0) {
+    snprintf(buf, sizeof(buf), "(error code %d)", errnum);
+  }
+  errmsg = buf;
+#endif /* defined(_WIN32) */
+
+  caml_failwith_value(caml_alloc_sprintf(fmtmsg, errmsg));
+}
+
+static int unsn_is_system_attr_os(const char *attrname)
+{
+#if defined(__linux)
+  return (strncmp(attrname, "system.", 7) == 0 &&
+          strncmp(attrname, "system.posix_acl_", 17) != 0);
+#elif defined(__APPLE__)
+  return (strcmp(attrname, XATTR_FINDERINFO_NAME) == 0 ||
+          strcmp(attrname, XATTR_RESOURCEFORK_NAME) == 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return 0;
+#elif defined(__Solaris__)
+  /* Special system "extensible attributes" xattrs are defined in sys/attr.h
+   * as VIEW_READONLY = "SUNWattr_ro" and VIEW_READWRITE = "SUNWattr_rw" */
+  return (strcmp(attrname, ".") == 0 || strcmp(attrname, "..") == 0 ||
+          strncmp(attrname, "SUNWattr_", 9) == 0);
+#endif
+}
+
+
+/************************************
+ *            Set xattr
+ ************************************/
+static int unsn_set_xattr_os(const char *path, const char *attrname,
+                             const void *attrvalue, size_t valuesize)
+{
+#if defined(__linux)
+  return setxattr(path, attrname, attrvalue, valuesize, 0);
+#elif defined(__APPLE__)
+  return setxattr(path, attrname, attrvalue, valuesize, 0, 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return (int) extattr_set_file(path, EXTATTR_NAMESPACE_USER, attrname,
+                                attrvalue, valuesize);
+#elif defined(__Solaris__)
+  if (pathconf(path, _PC_XATTR_ENABLED) < 1) {
+    unsn_xattr_not_supported();
+  }
+
+  /* This is a simplified implementation that just creates/opens
+   * the xattr and writes the value into it.
+   *
+   * Extended attributes in Solaris and illumos are much more
+   * flexible. In most ways they are like normal files/directories.
+   * They have owner/group, mode, utimes, even ACL, and can have
+   * their own extended attributes, etc.
+   *
+   * This implementation does not synchronize any of those params,
+   * as xattrs are conceptually treated as name-value pairs.
+   * It is unknown if this will cause problems with real use cases. */
+  int fd = attropen(path, attrname, O_CREAT|O_WRONLY|O_TRUNC|O_CLOEXEC);
+  if (fd == -1) {
+    unsn_xattr_fail("Error opening extended attribute for writing: %s");
+  }
+
+  ssize_t written = 0, c;
+  do {
+    c = write(fd, attrvalue + written, valuesize - written);
+    written += c;
+  } while (c > 0 && written < valuesize);
+
+  close(fd);
+
+  return c == -1 ? c : 0;
+#endif
+}
+
+CAMLprim value unison_xattr_set(value path, value xattrname, value xattr)
+{
+  CAMLparam3(path, xattrname, xattr);
+  const char *name = String_val(path);
+  XATTRNAME_VAL(attr, xattrname);
+  const char *attrvalue = String_val(xattr);
+  unsigned int len;
+
+  /* Ignore system extended attributes */
+  if (unsn_is_system_attr_os(attr)) {
+    CAMLreturn(Val_unit);
+  }
+
+  len = caml_string_length(xattr);
+
+  int error = unsn_set_xattr_os(name, attr, attrvalue, len);
+  if (error == -1) {
+    if (errno == ENOTSUP) {
+      unsn_xattr_not_supported();
+    } else {
+      unsn_xattr_fail("Error writing extended attribute: %s");
+    }
+  }
+
+  CAMLreturn(Val_unit);
+}
+
+
+/************************************
+ *           Remove xattr
+ ************************************/
+static int unsn_remove_xattr_os(const char *path, const char *attrname)
+{
+#if defined(__linux)
+  return removexattr(path, attrname);
+#elif defined(__APPLE__)
+  return removexattr(path, attrname, 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return (int) extattr_delete_file(path, EXTATTR_NAMESPACE_USER, attrname);
+#elif defined(__Solaris__)
+  if (pathconf(path, _PC_XATTR_ENABLED) < 1) {
+    unsn_xattr_not_supported();
+  }
+
+  int fd = attropen(path, ".", O_RDONLY|O_CLOEXEC);
+  if (fd == -1) {
+    unsn_xattr_fail("Error opening extended attribute for removing: %s");
+  }
+  int error = unlinkat(fd, attrname, 0);
+  close(fd);
+
+  return error;
+#endif
+}
+
+CAMLprim value unison_xattr_remove(value path, value xattrname)
+{
+  CAMLparam2(path, xattrname);
+  const char *name = String_val(path);
+  XATTRNAME_VAL(attr, xattrname);
+
+  /* Ignore system extended attributes */
+  if (unsn_is_system_attr_os(attr)) {
+    CAMLreturn(Val_unit);
+  }
+
+  int error = unsn_remove_xattr_os(name, attr);
+  if (error == -1 && errno == ENOTSUP) {
+    unsn_xattr_not_supported();
+  }
+
+  CAMLreturn(Val_unit);
+}
+
+
+/************************************
+ *         Length of xattr
+ ************************************/
+static ssize_t unsn_length_xattr_os(const char *path, const char *attrname)
+{
+#if defined(__linux)
+  return getxattr(path, attrname, NULL, 0);
+#elif defined(__APPLE__)
+  return getxattr(path, attrname, NULL, 0, 0, 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return extattr_get_file(path, EXTATTR_NAMESPACE_USER, attrname, NULL, 0);
+#elif defined(__Solaris__)
+  int fd = attropen(path, attrname, O_RDONLY|O_CLOEXEC);
+  if (fd == -1) {
+    unsn_xattr_fail("Error opening extended attribute for querying length: %s");
+  }
+
+  struct stat buf;
+  int error;
+
+  error = fstat(fd, &buf);
+  close(fd);
+
+  return error == -1 ? error : buf.st_size;
+#endif
+}
+
+
+/************************************
+ *            Get xattrs
+ ************************************/
+static ssize_t unsn_get_xattr_os(const char *path, const char *attrname,
+                                 void *buf, size_t size)
+{
+#if defined(__linux)
+  return getxattr(path, attrname, buf, size);
+#elif defined(__APPLE__)
+  return getxattr(path, attrname, buf, size, 0, 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return extattr_get_file(path, EXTATTR_NAMESPACE_USER, attrname, buf, size);
+#elif defined(__Solaris__)
+  int fd = attropen(path, attrname, O_RDONLY|O_CLOEXEC);
+  if (fd == -1) {
+    unsn_xattr_fail("Error opening extended attribute for reading: %s");
+  }
+
+  ssize_t rd = 0, c;
+  do {
+    c = read(fd, buf + rd, size - rd);
+    rd += c;
+  } while (c > 0 && rd < size);
+
+  close(fd);
+
+  return c == -1 ? c : rd;
+#endif
+}
+
+CAMLprim value unison_xattr_get(value path, value xattrname)
+{
+  CAMLparam2(path, xattrname);
+  CAMLlocal1(v);
+  const char *name = String_val(path);
+  XATTRNAME_VAL(attr, xattrname);
+
+  int len = 0, tries = 0;
+
+  do {
+    if (++tries > 10) {
+      caml_failwith("Error reading contents of extended attribute; "
+        "it keeps changing");
+    }
+
+    len = unsn_length_xattr_os(name, attr);
+    if (len == -1 && errno == ENOTSUP) {
+      unsn_xattr_not_supported();
+    } else if (len == -1) {
+      unsn_xattr_fail("Error reading length of extended attribute: %s");
+    }
+
+    if (len == 0) {
+      v = caml_alloc_string(0);
+    } else if (len > 16777211) { // Max OCaml string length on 32 bit platforms
+      caml_failwith_value(caml_alloc_sprintf(
+        "Extended attribute value is too big (%d bytes)", len));
+    } else {
+      char *buf = malloc(len);
+
+      len = unsn_get_xattr_os(name, attr, buf, len);
+      if (len == -1 && errno == ENOTSUP) {
+        free(buf);
+        unsn_xattr_not_supported();
+      } else if (len == -1 && errno != ERANGE) {
+        free(buf);
+        unsn_xattr_fail("Error reading contents of extended attribute: %s");
+      } else if (len != -1) {
+        v = caml_alloc_initialized_string(len, buf);
+      }
+
+      free(buf);
+    }
+  } while (len == -1 && errno == ERANGE);
+  /* ERANGE error produced on Linux and Darwin; other platforms
+   * truncate the value if buffer is too small. */
+
+  CAMLreturn(v);
+}
+
+
+/************************************
+ *           List xattrs
+ ************************************/
+static void unsn_list_xattr_fail(void)
+{
+  unsn_xattr_fail("Error getting list of extended attributes: %s");
+}
+
+#if !defined(__Solaris__)
+
+static ssize_t unsn_list_xattr_os(const char *path, char *buf, size_t size)
+{
+#if defined(__linux)
+  return listxattr(path, buf, size);
+#elif defined(__APPLE__)
+  return listxattr(path, buf, size, 0);
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+  return extattr_list_file(path, EXTATTR_NAMESPACE_USER, buf, size);
+#endif
+}
+
+static ssize_t unsn_list_xattr_aux(const char *path, char **buf)
+{
+  ssize_t namelen;
+
+  namelen = unsn_list_xattr_os(path, NULL, 0);
+
+  if (namelen == -1) {
+    if (errno == ENOTSUP) {
+      unsn_xattr_not_supported();
+    }
+    unsn_list_xattr_fail();
+  }
+  if (namelen == 0) {
+    return 0;
+  }
+
+  *buf = malloc(namelen);
+  if (*buf == NULL) {
+    unsn_list_xattr_fail();
+  }
+
+  namelen = unsn_list_xattr_os(path, *buf, namelen);
+  if (namelen == -1) {
+    free(*buf);
+    unsn_list_xattr_fail();
+  }
+  if (namelen == 0) {
+    free(*buf);
+    return 0;
+  }
+
+  return namelen;
+}
+
+#else
+
+static ssize_t unsn_list_xattr_aux(const char *path, DIR **dirp)
+{
+  if (pathconf(path, _PC_XATTR_ENABLED) < 1) {
+    unsn_xattr_not_supported();
+  }
+
+  if (pathconf(path, _PC_XATTR_EXISTS) < 1) {
+    return 0;
+  }
+
+  int fd = attropen(path, ".", O_RDONLY|O_CLOEXEC);
+  if (fd == -1) {
+    unsn_list_xattr_fail();
+  }
+
+  *dirp = fdopendir(fd);
+  if (*dirp == NULL) {
+    int real_err = errno;
+    close(fd);
+    errno = real_err;
+    unsn_list_xattr_fail();
+  }
+
+  return 1;
+}
+
+#endif /* !__Solaris__ */
+
+CAMLprim value unison_xattrs_list(value path)
+{
+  CAMLparam1(path);
+  CAMLlocal3(result, p, l);
+  /* Use a static buffer because memory management for the path would become
+   * much too complex. Use a constant length because PATH_MAX is not reliable
+   * and pathconf() is out of question. */
+  char name[32768];
+#if !defined(__Solaris__)
+  char *xattrs;
+#else
+  DIR *xattrs;
+  struct dirent *dp;
+  char *xattrname;
+#endif
+  ssize_t namelen, len;
+
+  if (caml_string_length(path) > 32767) {
+    caml_failwith("The path is too long");
+  }
+  strcpy(name, String_val(path));
+
+  result = Val_emptylist;
+
+  namelen = unsn_list_xattr_aux(name, &xattrs);
+  if (namelen == 0) {
+    CAMLreturn(result);
+  }
+
+#if defined(__FreeBSD__) || defined(__NetBSD__)
+  size_t nl = 0;
+  char xattrname[256];
+
+  for (char *xattrnamep = xattrs; xattrnamep < xattrs + namelen;
+                                  xattrnamep += nl + 1) {
+    nl = *xattrnamep & 255;
+    memcpy(xattrname, xattrnamep + 1, nl);
+    xattrname[nl] = '\0';
+#elif !defined(__Solaris__)
+  /* For safety */
+  *(xattrs + namelen - 1) = '\0';
+
+  for (char *xattrname = xattrs; xattrname < xattrs + namelen;
+                                 xattrname += strlen(xattrname) + 1) {
+#elif defined(__Solaris__)
+  while (dp = readdir(xattrs)) {
+    /* Note: NULL is returned for both end of dir and an error condition.
+     * Error conditions are silently ignored. */
+    xattrname = dp->d_name;
+#endif
+
+    /* Ignore system extended attributes */
+    if (unsn_is_system_attr_os(xattrname)) {
+      continue;
+    }
+
+    len = unsn_length_xattr_os(name, xattrname);
+    if (len == -1) {
+      continue; /* Ignore silently */
+    }
+
+    p = caml_alloc_tuple(2);
+    Store_field(p, 0, VAL_XATTRNAME(xattrname));
+    Store_field(p, 1, Val_int(len));
+
+    l = caml_alloc_small(2, Tag_cons);
+    Field(l, 0) = p;
+    Field(l, 1) = result;
+
+    result = l;
+  }
+
+#if defined(__Solaris__)
+  closedir(xattrs);
+#else
+  free(xattrs);
+#endif
+
+  CAMLreturn(result);
+}
+
+
+/************************************
+ *        ctime capabilities
+ ************************************/
+CAMLprim value unison_xattr_updates_ctime(value unit)
+{
+  CAMLparam0();
+#if defined(__Solaris__)
+  CAMLreturn(Val_false);
+#else
+  CAMLreturn(Val_true);
+#endif
+}
+
+
+#endif /* UNSN_HAS_XATTR */
diff --git a/src/propsdata.ml b/src/propsdata.ml
new file mode 100644
index 0000000..1f8ec86
--- /dev/null
+++ b/src/propsdata.ml
@@ -0,0 +1,112 @@
+(* Unison file synchronizer: src/propsdata.ml *)
+(* Copyright 2020-2022, Tõivo Leedjärv
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*)
+
+
+module type S = sig
+  val get : [< `All | `New | `Kept] -> (string * string) list
+  val set : (string * string) list -> unit
+  val merge : (string * string) list -> unit
+  val clear : [`Kept] -> unit
+end
+
+
+module KVStore (V : sig val initSize : int end) = struct
+
+(* Key-value store with a relatively low number of entries (in the tens
+   or hundreds, or at most in low thousands).
+
+   This is not a generic key-value store; this is specifically intended
+   for use by [Props.Data].
+
+   Several simple implementations are possible (for example, a Map or an
+   association list). There seems to be very little difference in terms
+   of performance. Hashtbl has been chosen as it may have a slight scaling
+   advantage. In practice, there probably are no tangile differences
+   between these simple implementations in most scenarios. *)
+let mainStore = Hashtbl.create V.initSize
+let newStore = Hashtbl.create V.initSize
+let keepStore = Hashtbl.create V.initSize
+
+let getStore = function
+  | `All -> mainStore
+  | `New -> newStore
+  | `Kept -> keepStore
+
+let exists key = Hashtbl.mem mainStore key
+
+let find_opt key = Hashtbl.find_opt mainStore key
+
+let associate key value = Hashtbl.add mainStore key value
+
+let associateNew key value =
+  associate key value;
+  Hashtbl.add newStore key value
+
+let add key value =
+  if not (exists key) then associateNew key value
+
+let find key =
+  match find_opt key with
+  | Some v -> v
+  | None -> assert false (* Indicates a bug *)
+
+let get kind =
+  Hashtbl.fold (fun key value acc -> (key, value) :: acc) (getStore kind) []
+
+let set d =
+  Hashtbl.clear mainStore;
+  Hashtbl.clear newStore;
+  Safelist.iter (fun (key, value) -> associate key value) d
+
+let associate_cmp key value =
+  match find_opt key with
+  | None -> associate key value
+  | Some v when v = value -> ()
+  | Some v ->
+      raise (Util.Fatal ("Internal integrity error (propsdata). Key " ^ key
+        ^ " returns different results:\n  (existing) " ^ v
+        ^ "\nand\n  (new) " ^ value ^ "\n"))
+
+let merge d =
+  Safelist.iter (fun (key, value) -> associate_cmp key value) d
+
+let clear kind =
+  Hashtbl.clear (getStore kind)
+
+let keep key =
+  if Hashtbl.mem keepStore key then ()
+  else Hashtbl.add keepStore key (find key)
+
+end (* module KVStore *)
+
+
+(* ------------------------------------------------------------------------- *)
+(*                       Extended attributes (xattr)                         *)
+(* ------------------------------------------------------------------------- *)
+
+module Xattr = struct
+  include KVStore (struct let initSize = 200 end)
+
+  let length () = Hashtbl.length mainStore
+end
+
+
+(* ------------------------------------------------------------------------- *)
+(*                                  ACL                                      *)
+(* ------------------------------------------------------------------------- *)
+
+module ACL = KVStore (struct let initSize = 25 end)
diff --git a/src/propsdata.mli b/src/propsdata.mli
new file mode 100644
index 0000000..c6123cb
--- /dev/null
+++ b/src/propsdata.mli
@@ -0,0 +1,25 @@
+(* Unison file synchronizer: src/propsdata.mli *)
+(* Copyright 2022, Tõivo Leedjärv (see COPYING for details) *)
+
+module type S = sig
+  val get : [< `All | `New | `Kept] -> (string * string) list
+  val set : (string * string) list -> unit
+  val merge : (string * string) list -> unit
+  val clear : [`Kept] -> unit
+end
+
+module Xattr : sig
+  include S
+
+  val add : string -> string -> unit
+  val find_opt : string -> string option
+  val length : unit -> int
+end
+
+module ACL : sig
+  include S
+
+  val add : string -> string -> unit
+  val find : string -> string
+  val keep : string -> unit
+end
diff --git a/src/pty.c b/src/pty.c
index 010b71f..a14d1b7 100644
--- a/src/pty.c
+++ b/src/pty.c
@@ -11,13 +11,24 @@
 
 #endif /* _WIN32 */
 
-#define CAML_NAME_SPACE
 #include <caml/mlvalues.h>
 #include <caml/alloc.h>    // alloc_tuple
 #include <caml/memory.h>   // Store_field
 #include <caml/fail.h>     // failwith
 #include <caml/unixsupport.h> // uerror, unix_error
 #include <errno.h>         // ENOSYS
+#include <caml/version.h>
+#if OCAML_VERSION < 41300
+#define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */
+#endif
+#include <caml/osdeps.h>
+
+#if OCAML_VERSION_MAJOR < 5
+#define caml_unix_error unix_error
+#define caml_uerror uerror
+#define caml_win32_maperr win32_maperr
+#define caml_win32_alloc_handle win_alloc_handle
+#endif
 
 // openpty
 #if defined(__linux)
@@ -45,17 +56,17 @@ CAMLprim value setControllingTerminal(value fdVal) {
   CAMLparam1(fdVal);
   int fd = Int_val(fdVal);
   if (ioctl(fd, TIOCSCTTY, (char *) 0) < 0)
-    uerror("ioctl", (value) 0);
+    caml_uerror("ioctl", (value) 0);
   CAMLreturn(Val_unit);
 }
 
 /* c_openpty: unit -> (int * Unix.file_descr) */
-CAMLprim value c_openpty() {
+CAMLprim value c_openpty(value unit) {
   CAMLparam0();
-  int master,slave;
   CAMLlocal1(pair);
+  int master, slave;
   if (openpty(&master,&slave,NULL,NULL,NULL) < 0)
-    uerror("openpty", (value) 0);
+    caml_uerror("openpty", (value) 0);
   pair = caml_alloc_tuple(2);
   Store_field(pair,0,Val_int(master));
   Store_field(pair,1,Val_int(slave));
@@ -65,74 +76,19 @@ CAMLprim value c_openpty() {
 #else // not HAS_OPENPTY
 
 CAMLprim value setControllingTerminal(value fdVal) {
-  unix_error (ENOSYS, "setControllingTerminal", Nothing);
+  caml_unix_error(ENOSYS, "setControllingTerminal", Nothing);
 }
 
-CAMLprim value c_openpty() {
-  unix_error (ENOSYS, "openpty", Nothing);
+CAMLprim value c_openpty(value unit) {
+  caml_unix_error(ENOSYS, "openpty", Nothing);
 }
 
 #endif
 
 #ifdef _WIN32
 
-#ifndef CAMLassert
-#define CAMLassert(x) ((void) 0)
-#endif
-
 #include <windows.h>
 
-extern void win32_maperr(DWORD errcode);
-extern value win_alloc_handle(HANDLE h);
-
-static int Twin_multi_byte_to_wide_char(const char *s, int slen,
-                                        wchar_t *out, int outlen)
-{
-  int retcode;
-
-  CAMLassert (s != NULL);
-
-  if (slen == 0)
-    return 0;
-
-  retcode =
-    MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
-                        s, slen, out, outlen);
-  if (retcode == 0)
-    retcode = MultiByteToWideChar(CP_ACP, 0, s, slen, out, outlen);
-
-  if (retcode == 0) {
-    win32_maperr(GetLastError());
-    uerror("", Nothing);
-  }
-
-  return retcode;
-}
-
-#ifndef Bytes_val /* Hack to know that we are on OCaml < 4.06.
-                     #include <caml/version.h> is not always found, for some reason. */
-static void* caml_stat_alloc_noexc(asize_t sz)
-{
-  return malloc(sz);
-}
-#endif /* OCAML_VERSION < 40600 */
-
-static wchar_t* Tcaml_stat_strdup_to_utf16(const char *s)
-{
-  wchar_t * ws;
-  int retcode;
-
-  retcode = Twin_multi_byte_to_wide_char(s, -1, NULL, 0);
-  ws = caml_stat_alloc_noexc(retcode * sizeof(*ws));
-  Twin_multi_byte_to_wide_char(s, -1, ws, retcode);
-
-  return ws;
-}
-
-#ifndef Data_abstract_val /* OCaml < 4.05 */
-#define Data_abstract_val(v) ((void*) Op_val(v))
-#endif
-
 #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 0x00020016
 
 typedef VOID* HPCON;
@@ -145,7 +101,7 @@ typedef void (WINAPI *sClosePseudoConsole) (HPCON hPC);
 sCreatePseudoConsole pCreatePseudoConsole;
 sClosePseudoConsole pClosePseudoConsole;
 
-CAMLprim value win_openpty()
+CAMLprim value win_openpty(value unit)
 {
   CAMLparam0();
   CAMLlocal4(tup, tmp1, tmp2, tmp3);
@@ -155,7 +111,7 @@ CAMLprim value win_openpty()
 
   HMODULE kernel32_module = GetModuleHandleW(L"kernel32.dll");
   if (kernel32_module == NULL) {
-    unix_error(ENOSYS, "openpty", Nothing);
+    caml_unix_error(ENOSYS, "openpty", Nothing);
   }
 
   /* This is the only way to use the new API while remaining compatible
@@ -163,7 +119,7 @@ CAMLprim value win_openpty()
   pCreatePseudoConsole = (sCreatePseudoConsole)
       GetProcAddress(kernel32_module, "CreatePseudoConsole");
   if (pCreatePseudoConsole == NULL) {
-    unix_error(ENOSYS, "openpty", Nothing);
+    caml_unix_error(ENOSYS, "openpty", Nothing);
   }
 
   /* Read-write pipes don't seem to work well with PTY and cause deadlocks.
@@ -172,35 +128,35 @@ CAMLprim value win_openpty()
    * emulation by PF_UNIX sockets is only supported starting Windows 10 1803).
    * Simpler to use two separate pipes then. */
   if (!CreatePipe(&i1, &o1, NULL, 0)) {
-    win32_maperr(GetLastError());
-    uerror("openpty", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("openpty", Nothing);
   }
   if (!CreatePipe(&i2, &o2, NULL, 0)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     if (o1 != INVALID_HANDLE_VALUE) CloseHandle(o1);
     if (i1 != INVALID_HANDLE_VALUE) CloseHandle(i1);
-    uerror("openpty", Nothing);
+    caml_uerror("openpty", Nothing);
   }
 
   if (pCreatePseudoConsole(size, i1, o2, 0, &pty) != S_OK) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     if (o1 != INVALID_HANDLE_VALUE) CloseHandle(o1);
     if (i1 != INVALID_HANDLE_VALUE) CloseHandle(i1);
     if (o2 != INVALID_HANDLE_VALUE) CloseHandle(o2);
     if (i2 != INVALID_HANDLE_VALUE) CloseHandle(i2);
-    uerror("openpty", Nothing);
+    caml_uerror("openpty", Nothing);
   }
 
   tmp1 = caml_alloc_tuple(2);
-  Store_field(tmp1, 0, win_alloc_handle(i2));
-  Store_field(tmp1, 1, win_alloc_handle(o1));
+  Store_field(tmp1, 0, caml_win32_alloc_handle(i2));
+  Store_field(tmp1, 1, caml_win32_alloc_handle(o1));
 
   tmp2 = caml_alloc(1, Abstract_tag);
   *((HPCON *) Data_abstract_val(tmp2)) = pty;
 
   tmp3 = caml_alloc_tuple(2);
-  Store_field(tmp3, 0, win_alloc_handle(i1));
-  Store_field(tmp3, 1, win_alloc_handle(o2));
+  Store_field(tmp3, 0, caml_win32_alloc_handle(i1));
+  Store_field(tmp3, 1, caml_win32_alloc_handle(o2));
 
   tup = caml_alloc_tuple(3);
   Store_field(tup, 0, tmp1);
@@ -216,7 +172,7 @@ CAMLprim value win_closepty(value pty)
 
   HMODULE kernel32_module = GetModuleHandleW(L"kernel32.dll");
   if (kernel32_module == NULL) {
-    unix_error(ENOSYS, "closepty", Nothing);
+    caml_unix_error(ENOSYS, "closepty", Nothing);
   }
 
   /* This is the only way to use the new API while remaining compatible
@@ -224,7 +180,7 @@ CAMLprim value win_closepty(value pty)
   pClosePseudoConsole = (sClosePseudoConsole)
       GetProcAddress(kernel32_module, "ClosePseudoConsole");
   if (pClosePseudoConsole == NULL) {
-    unix_error(ENOSYS, "closepty", Nothing);
+    caml_unix_error(ENOSYS, "closepty", Nothing);
   }
 
   pClosePseudoConsole(*((HPCON *) Data_abstract_val(pty)));
@@ -235,7 +191,7 @@ CAMLprim value win_closepty(value pty)
 static void prepareSiWithPty(value prog, STARTUPINFOEX *si, HPCON pty)
 {
 #ifndef PROC_THREAD_ATTRIBUTE_HANDLE_LIST
-  unix_error(ENOSYS, "create_process_pty", prog);
+  caml_unix_error(ENOSYS, "create_process_pty", prog);
 #else
   SIZE_T size;
 
@@ -246,21 +202,21 @@ static void prepareSiWithPty(value prog, STARTUPINFOEX *si, HPCON pty)
   InitializeProcThreadAttributeList(NULL, 2, 0, &size);
   si->lpAttributeList = malloc(size);
   if (!si->lpAttributeList) {
-    unix_error(ENOMEM, "create_process_pty", prog);
+    caml_unix_error(ENOMEM, "create_process_pty", prog);
   }
 
   if (!InitializeProcThreadAttributeList(si->lpAttributeList, 2, 0, &size)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     free(si->lpAttributeList);
-    uerror("create_process_pty", prog);
+    caml_uerror("create_process_pty", prog);
   }
 
   if (!UpdateProcThreadAttribute(si->lpAttributeList, 0,
       PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pty, sizeof(pty), NULL, NULL)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     DeleteProcThreadAttributeList(si->lpAttributeList);
     free(si->lpAttributeList);
-    uerror("create_process_pty", prog);
+    caml_uerror("create_process_pty", prog);
   }
 #endif
 }
@@ -281,15 +237,15 @@ CAMLprim value w_create_process_pty_native
   HPCON hpc = *((HPCON *) Data_abstract_val(pty));
 
 #ifndef PROC_THREAD_ATTRIBUTE_HANDLE_LIST
-  unix_error(ENOSYS, "create_process_pty", prog);
+  caml_unix_error(ENOSYS, "create_process_pty", prog);
 #else
-  wprog = Tcaml_stat_strdup_to_utf16(String_val(prog));
+  wprog = caml_stat_strdup_to_utf16(String_val(prog));
 
   res = SearchPathW(NULL, wprog, L".exe", MAX_PATH, fullname, NULL);
   caml_stat_free(wprog);
   if (res == 0) {
-    win32_maperr(GetLastError());
-    uerror("create_process_pty", prog);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("create_process_pty", prog);
   }
 
   prepareSiWithPty(prog, &si, hpc);
@@ -297,19 +253,19 @@ CAMLprim value w_create_process_pty_native
   hp = GetCurrentProcess();
   if (!DuplicateHandle(hp, Handle_val(fd1), hp, &(si.StartupInfo.hStdInput),
       0, TRUE, DUPLICATE_SAME_ACCESS)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     err = TRUE;
     goto clean1;
   }
   if (!DuplicateHandle(hp, Handle_val(fd2), hp, &(si.StartupInfo.hStdOutput),
       0, TRUE, DUPLICATE_SAME_ACCESS)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     err = TRUE;
     goto clean2;
   }
   if (!DuplicateHandle(hp, Handle_val(fd3), hp, &(si.StartupInfo.hStdError),
       0, TRUE, DUPLICATE_SAME_ACCESS)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     err = TRUE;
     goto clean3;
   }
@@ -321,7 +277,7 @@ CAMLprim value w_create_process_pty_native
   if (!UpdateProcThreadAttribute(si.lpAttributeList, 0,
       PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherit, sizeof(HANDLE) * 3,
       NULL, NULL)) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     err = TRUE;
     goto clean4;
   }
@@ -329,12 +285,12 @@ CAMLprim value w_create_process_pty_native
   flags = GetPriorityClass(GetCurrentProcess());
   flags |= EXTENDED_STARTUPINFO_PRESENT;
 
-  wargs = Tcaml_stat_strdup_to_utf16(String_val(args));
+  wargs = caml_stat_strdup_to_utf16(String_val(args));
   res = CreateProcessW(fullname, wargs, NULL, NULL, TRUE, flags,
           NULL, NULL, &si.StartupInfo, &pi);
   caml_stat_free(wargs);
   if (res == 0) {
-    win32_maperr(GetLastError());
+    caml_win32_maperr(GetLastError());
     err = TRUE;
   }
 
@@ -349,7 +305,7 @@ clean1:
   free(si.lpAttributeList);
 
   if (err) {
-    uerror("create_process_pty", prog);
+    caml_uerror("create_process_pty", prog);
   }
 
   CloseHandle(pi.hThread);
@@ -363,27 +319,64 @@ CAMLprim value w_create_process_pty(value *argv, int argn)
                                      argv[3], argv[4], argv[5]);
 }
 
+CAMLprim value win_alloc_console(value unit)
+{
+  CAMLparam0();
+  CAMLlocal1(some);
+  HANDLE stderr_orig;
+  FILE *ign;
+
+  stderr_orig = (HANDLE) GetStdHandle(STD_ERROR_HANDLE);
+
+  if (!AllocConsole()) {
+    caml_win32_maperr(GetLastError());
+    caml_uerror("alloc_console", Nothing);
+  }
+
+  /* If a new console was allocated then we need to make sure that both the
+   * Windows C runtime stderr and the STD_ERROR_HANDLE for the child process
+   * are associated with the new console, unless already redirected or
+   * associated elsewhere.
+   * We are not interested in stdin and stdout for this specific scenario
+   * (as a fallback for the real pty). */
+  if (_fileno(stderr) < 0) freopen_s(&ign, "CONOUT$", "w", stderr);
+
+  if (!GetFileType(stderr_orig)) {
+    some = caml_alloc(1, 0);
+    Store_field(some, 0,
+      caml_win32_alloc_handle((HANDLE) GetStdHandle(STD_ERROR_HANDLE)));
+    CAMLreturn(some);
+  }
+
+  CAMLreturn(Val_int(0));
+}
+
 #else // not _WIN32
 
 CAMLprim value w_create_process_pty_native
   (value prog, value args, value pty, value fd1, value fd2, value fd3)
 {
-  unix_error(ENOSYS, "create_process_pty", Nothing);
+  caml_unix_error(ENOSYS, "create_process_pty", Nothing);
 }
 
 CAMLprim value w_create_process_pty(value *argv, int argn)
 {
-  unix_error(ENOSYS, "create_process_pty", Nothing);
+  caml_unix_error(ENOSYS, "create_process_pty", Nothing);
 }
 
 CAMLprim value win_openpty()
 {
-  unix_error(ENOSYS, "openpty", Nothing);
+  caml_unix_error(ENOSYS, "openpty", Nothing);
 }
 
 CAMLprim value win_closepty(value pty)
 {
-  unix_error(ENOSYS, "closepty", Nothing);
+  caml_unix_error(ENOSYS, "closepty", Nothing);
+}
+
+CAMLprim value win_alloc_console()
+{
+  caml_unix_error(ENOSYS, "alloc_console", Nothing);
 }
 
 #endif
diff --git a/src/recon.ml b/src/recon.ml
index 57f495a..a3ba18b 100644
--- a/src/recon.ml
+++ b/src/recon.ml
@@ -66,8 +66,23 @@ let revertToDefaultDirection ri =
 (*                   'Replica2ToReplica1                                     *)
 (* --                                                                        *)
 let root2direction root =
+  let partialMatch s = function
+    | Clroot.ConnectLocal (None | Some "") -> false
+    | Clroot.ConnectLocal (Some root) ->
+        Util.startswith root s || Util.endswith root s
+    | ConnectByShell (_, host, _, _, Some root)
+    | ConnectBySocket (host, _, Some root) ->
+        Util.startswith root s || Util.endswith root s || Util.startswith host s
+    | ConnectByShell (_, host, _, _, None)
+    | ConnectBySocket (host, _, None) ->
+        Util.startswith host s
+  in
+  let partialRootMatches prefVal =
+    Safelist.map (partialMatch prefVal) (Globals.parsedClRawRoots ())
+  in
   if      root="older" then `Older
   else if root="newer" then `Newer
+  else if root = "" then `None
   else
     let (r1, r2) = Globals.rawRootPair () in
     debug (fun() ->
@@ -75,18 +90,38 @@ let root2direction root =
          root r1 r2);
     if r1 = root then `Replica1ToReplica2 else
     if r2 = root then `Replica2ToReplica1 else
-    raise (Util.Fatal (Printf.sprintf
-     "%s (given as argument to 'prefer' or 'force' preference)\nis not one of \
-      the current roots:\n  %s\n  %s" root r1 r2))
+    match partialRootMatches root with
+    | [true; false] -> `Replica1ToReplica2
+    | [false; true] -> `Replica2ToReplica1
+    | _ ->
+        raise (Util.Fatal (Printf.sprintf "%s\nis not uniquely identifying one \
+          of the current roots:\n  %s\n  %s" root r1 r2))
+
+let rootDirCache = ref []
+
+let clearRootDirCache () = rootDirCache := []
+
+let prefRoot prefV =
+  (* Use physical equality with cache keys. The goal is not to avoid as many
+     cache misses as possible but to make cache checking much cheaper than
+     calculating the value (in this case, hashing and string comparison are
+     not quite cheap enough). *)
+  match List.assq_opt prefV !rootDirCache with
+  | Some x -> x
+  | None -> let x = root2direction prefV in
+            rootDirCache := (prefV, x) :: !rootDirCache; x
 
 let forceRoot: string Prefs.t =
   Prefs.createString "force" ""
-    "!force changes from this replica to the other"
+    ~category:(`Advanced `Sync)
+    "force changes from this replica to the other"
     ("Including the preference \\texttt{-force \\ARG{root}} causes Unison to "
      ^ "resolve all differences (even non-conflicting changes) in favor of "
      ^ "\\ARG{root}.  "
      ^ "This effectively changes Unison from a synchronizer into a mirroring "
      ^ "utility.  \n\n"
+     ^ "You can also specify a unique prefix or suffix of the path of one of "
+     ^ "the roots or a unique prefix of the hostname of a remote root.\n\n"
      ^ "You can also specify \\verb|-force newer| (or \\verb|-force older|) "
      ^ "to force Unison to choose the file with the later (earlier) "
      ^ "modtime.  In this case, the \\verb|-times| preference must also "
@@ -96,15 +131,18 @@ let forceRoot: string Prefs.t =
      ^ "know what you are doing!")
 
 let forceRootPartial: Pred.t =
-  Pred.create "forcepartial" ~advanced:true
+  Pred.create "forcepartial"
+    ~category:(`Advanced `Sync)
     ("Including the preference \\texttt{forcepartial = \\ARG{PATHSPEC} -> \\ARG{root}} causes Unison to "
      ^ "resolve all differences (even non-conflicting changes) in favor of "
      ^ "\\ARG{root} for the files in \\ARG{PATHSPEC} (see \\sectionref{pathspec}{Path Specification} "
      ^ "for more information).  "
      ^ "This effectively changes Unison from a synchronizer into a mirroring "
      ^ "utility.  \n\n"
+     ^ "You can also specify a unique prefix or suffix of the path of one of "
+     ^ "the roots or a unique prefix of the hostname of a remote root.\n\n"
      ^ "You can also specify \\verb|forcepartial PATHSPEC -> newer| "
-     ^ "(or \\verb|forcepartial PATHSPEC older|) "
+     ^ "(or \\verb|forcepartial PATHSPEC -> older|) "
      ^ "to force Unison to choose the file with the later (earlier) "
      ^ "modtime.  In this case, the \\verb|-times| preference must also "
      ^ "be enabled.\n\n"
@@ -113,19 +151,23 @@ let forceRootPartial: Pred.t =
 
 let preferRoot: string Prefs.t =
   Prefs.createString "prefer" ""
-    "!choose this replica's version for conflicting changes"
+    ~category:(`Advanced `Sync)
+    "choose this replica's version for conflicting changes"
     ("Including the preference \\texttt{-prefer \\ARG{root}} causes Unison always to "
      ^ "resolve conflicts in favor of \\ARG{root}, rather than asking for "
      ^ "guidance from the user, except for paths marked by the preference "
      ^ "\\texttt{merge}.  (The syntax of \\ARG{root} is the same as "
      ^ "for the \\verb|root| preference, plus the special values "
      ^ "\\verb|newer| and \\verb|older|.)  \n\n"
+     ^ "You can also specify a unique prefix or suffix of the path of one of "
+     ^ "the roots or a unique prefix of the hostname of a remote root.\n\n"
      ^ "This preference is overridden by the \\verb|preferpartial| preference.\n\n"
      ^ "This preference should be used only if you are {\\em sure} you "
      ^ "know what you are doing!")
 
 let preferRootPartial: Pred.t =
-  Pred.create "preferpartial" ~advanced:true
+  Pred.create "preferpartial"
+    ~category:(`Advanced `Sync)
     ("Including the preference \\texttt{preferpartial = \\ARG{PATHSPEC} -> \\ARG{root}} "
      ^ "causes Unison always to "
      ^ "resolve conflicts in favor of \\ARG{root}, rather than asking for "
@@ -134,6 +176,8 @@ let preferRootPartial: Pred.t =
      ^ "for more information).  (The syntax of \\ARG{root} is the same as "
      ^ "for the \\verb|root| preference, plus the special values "
      ^ "\\verb|newer| and \\verb|older|.)  \n\n"
+     ^ "You can also specify a unique prefix or suffix of the path of one of "
+     ^ "the roots or a unique prefix of the hostname of a remote root.\n\n"
      ^ "This preference should be used only if you are {\\em sure} you "
      ^ "know what you are doing!")
 
@@ -141,50 +185,60 @@ let preferRootPartial: Pred.t =
 (* preferences "force"/"preference", returns a pair (root, force)            *)
 let lookupPreferredRoot () =
   if Prefs.read forceRoot <> "" then
-    (Prefs.read forceRoot, `Force)
+    (prefRoot (Prefs.read forceRoot), `Force)
   else if Prefs.read preferRoot <> "" then
-    (Prefs.read preferRoot, `Prefer)
+    (prefRoot (Prefs.read preferRoot), `Prefer)
   else
-    ("",`Prefer)
+    (`None, `Prefer)
 
 (* [lookupPreferredRootPartial: Path.t -> string * [`Force | `Prefer]] checks validity of  *)
 (* preferences "forcepartial", returns a pair (root, force)                                *)
 let lookupPreferredRootPartial p =
   let s = Path.toString p in
   if Pred.test forceRootPartial s then
-    (Pred.assoc forceRootPartial s, `Force)
+    (prefRoot (Pred.assoc forceRootPartial s), `Force)
   else if Pred.test preferRootPartial s then
-    (Pred.assoc preferRootPartial s, `Prefer)
+    (prefRoot (Pred.assoc preferRootPartial s), `Prefer)
   else
-    ("",`Prefer)
+    (`None, `Prefer)
 
 let noDeletion =
   Prefs.createStringList "nodeletion"
+    ~category:(`Basic `Sync)
     "prevent file deletions on one replica"
     ("Including the preference \\texttt{-nodeletion \\ARG{root}} prevents \
       Unison from performing any file deletion on root \\ARG{root}.\n\n\
+      You can also specify a unique prefix or suffix of the path of one of \
+      the roots or a unique prefix of the hostname of a remote root.\n\n\
       This preference can be included twice, once for each root, if you \
       want to prevent any deletion.")
 
 let noUpdate =
   Prefs.createStringList "noupdate"
+    ~category:(`Basic `Sync)
     "prevent file updates and deletions on one replica"
     ("Including the preference \\texttt{-noupdate \\ARG{root}} prevents \
       Unison from performing any file update or deletion on root \
       \\ARG{root}.\n\n\
+      You can also specify a unique prefix or suffix of the path of one of \
+      the roots or a unique prefix of the hostname of a remote root.\n\n\
       This preference can be included twice, once for each root, if you \
       want to prevent any update.")
 
 let noCreation =
   Prefs.createStringList "nocreation"
+    ~category:(`Basic `Sync)
     "prevent file creations on one replica"
     ("Including the preference \\texttt{-nocreation \\ARG{root}} prevents \
       Unison from performing any file creation on root \\ARG{root}.\n\n\
+      You can also specify a unique prefix or suffix of the path of one of \
+      the roots or a unique prefix of the hostname of a remote root.\n\n\
       This preference can be included twice, once for each root, if you \
       want to prevent any creation.")
 
 let noDeletionPartial =
-  Pred.create "nodeletionpartial" ~advanced:true
+  Pred.create "nodeletionpartial"
+    ~category:(`Advanced `Sync)
     ("Including the preference \
       \\texttt{nodeletionpartial = \\ARG{PATHSPEC} -> \\ARG{root}} prevents \
       Unison from performing any file deletion in \\ARG{PATHSPEC} \
@@ -193,7 +247,8 @@ let noDeletionPartial =
       patterns when selecting a directory and all its contents.")
 
 let noUpdatePartial =
-  Pred.create "noupdatepartial" ~advanced:true
+  Pred.create "noupdatepartial"
+    ~category:(`Advanced `Sync)
     ("Including the preference \
       \\texttt{noupdatepartial = \\ARG{PATHSPEC} -> \\ARG{root}} prevents \
       Unison from performing any file update or deletion in \
@@ -203,7 +258,8 @@ let noUpdatePartial =
       patterns when selecting a directory and all its contents.")
 
 let noCreationPartial =
-  Pred.create "nocreationpartial" ~advanced:true
+  Pred.create "nocreationpartial"
+    ~category:(`Advanced `Sync)
     ("Including the preference \
       \\texttt{nocreationpartial = \\ARG{PATHSPEC} ->  \\ARG{root}} prevents \
       Unison from performing any file creation in \\ARG{PATHSPEC} \
@@ -214,24 +270,33 @@ let noCreationPartial =
 
 let maxSizeThreshold =
   Prefs.createInt "maxsizethreshold" (-1)
-    "!prevent transfer of files bigger than this (if >=0, in Kb)"
+    ~category:(`Advanced `General)
+    "prevent transfer of files bigger than this (if >=0, in Kb)"
     ("A number indicating above what filesize (in kilobytes) Unison should "
      ^ "flag a conflict instead of transferring the file. "
      ^ "This conflict remains even in the presence of force or prefer options. "
      ^ "A negative number will allow every transfer independently of the size.  "
      ^ "The default is -1. ")
 
-let partialCancelPref actionKind =
-  match actionKind with
-    `DELETION -> noDeletionPartial
-  | `UPDATE   -> noUpdatePartial
-  | `CREATION -> noCreationPartial
-
-let cancelPref actionKind =
-  match actionKind with
-    `DELETION -> noDeletion
-  | `UPDATE   -> noUpdate
-  | `CREATION -> noCreation
+let testPartialCancelPref root path actionKind =
+  let partialCancelPref actionKind =
+    match actionKind with
+      `DELETION -> noDeletionPartial
+    | `UPDATE   -> noUpdatePartial
+    | `CREATION -> noCreationPartial
+  in
+  Pred.assoc_all (partialCancelPref actionKind) path
+  |> List.exists (fun x -> root = prefRoot x)
+
+let testCancelPref root actionKind =
+  let cancelPref actionKind =
+    match actionKind with
+      `DELETION -> noDeletion
+    | `UPDATE   -> noUpdate
+    | `CREATION -> noCreation
+  in
+  Prefs.read (cancelPref actionKind)
+  |> List.exists (fun x -> root = prefRoot x)
 
 let actionKind fromRc toRc =
   let fromTyp = fromRc.typ in
@@ -240,11 +305,11 @@ let actionKind fromRc toRc =
   if toTyp = `ABSENT then `CREATION else
   `DELETION
 
-let shouldCancel path rc1 rc2 root2 =
+let shouldCancel path rc1 rc2 root =
   let test kind =
-    List.mem root2 (Prefs.read (cancelPref kind))
+    testCancelPref root kind
       ||
-    List.mem root2 (Pred.assoc_all (partialCancelPref kind) path)
+    testPartialCancelPref root path kind
   in
   let testSize rc =
        Prefs.read maxSizeThreshold >= 0
@@ -264,7 +329,7 @@ let shouldCancel path rc1 rc2 root2 =
      if test `CREATION then true, "would create a file with nocreation or nocreationpartial set"
      else testSize rc1, "would transfer a file of size greater than maxsizethreshold"
 
-let filterRi root1 root2 ri =
+let filterRi ri =
   match ri.replicas with
     Problem _ ->
       ()
@@ -272,9 +337,9 @@ let filterRi root1 root2 ri =
      let cancel,reason =
        match diff.direction with
          Replica1ToReplica2 ->
-          shouldCancel (Path.toString ri.path1) diff.rc1 diff.rc2 root2
+          shouldCancel (Path.toString ri.path1) diff.rc1 diff.rc2 `Replica2ToReplica1
        | Replica2ToReplica1 ->
-          shouldCancel (Path.toString ri.path1) diff.rc2 diff.rc1 root1
+          shouldCancel (Path.toString ri.path1) diff.rc2 diff.rc1 `Replica1ToReplica2
        | Conflict _ | Merge ->
           false,""
      in
@@ -283,57 +348,51 @@ let filterRi root1 root2 ri =
        diff.direction <- Conflict reason
 
 let filterRis ris =
-  let (root1, root2) = Globals.rawRootPair () in
-  Safelist.iter (fun ri -> filterRi root1 root2 ri) ris
+  Safelist.iter filterRi ris
 
 (* Use the current values of the '-prefer <ROOT>' and '-force <ROOT>'        *)
 (* preferences to override the reconciler's choices                          *)
 let overrideReconcilerChoices ris =
-  let (root,force) = lookupPreferredRoot() in
-  if root<>"" then begin
-    let dir = root2direction root in
-    Safelist.iter (fun ri -> setDirection ri dir force) ris
-  end;
+  clearRootDirCache ();
+  let (dir, force) = lookupPreferredRoot () in
+  if dir <> `None then Safelist.iter (fun ri -> setDirection ri dir force) ris;
   Safelist.iter (fun ri ->
-                   let (rootp,forcep) = lookupPreferredRootPartial ri.path1 in
-                   if rootp<>"" then begin
-                     let dir = root2direction rootp in
-                       setDirection ri dir forcep
-                   end) ris;
+                   let (dir, forcep) = lookupPreferredRootPartial ri.path1 in
+                   if dir <> `None then setDirection ri dir forcep) ris;
   filterRis ris
 
 (* Look up the preferred root and verify that it is OK (this is called at    *)
 (* the beginning of the run, so that we don't have to wait to hear about     *)
 (* errors                                                                    *)
 let checkThatPreferredRootIsValid () =
-  let test_root predname = function
-    | "" | "newer" -> ()
-    | "older" as r ->
+  let test_root explicitRoot predname predvalue =
+    match prefRoot predvalue with
+    | `None | `Replica1ToReplica2 | `Replica2ToReplica1 -> ()
+    | (`Newer | `Older) when explicitRoot ->
+        raise (Util.Fatal ("Argument to preference '" ^ predname ^ "': "
+          ^ predvalue ^ " must not be keyword 'older' or 'newer'."))
+    | `Newer -> ()
+    | `Older ->
         if not (Prefs.read Props.syncModtimes) then
           raise (Util.Transient (Printf.sprintf
-                                   "The '%s=%s' preference can only be used with 'times=true'"
-                                   predname r))
-    | r -> ignore (root2direction r) in
-  let (root,pred) = lookupPreferredRoot() in
-  if root<>"" then test_root (match pred with `Force -> "force" | `Prefer -> "prefer") root;
-  Safelist.iter (test_root "forcepartial") (Pred.extern_associated_strings forceRootPartial);
-  Safelist.iter (test_root "preferpartial") (Pred.extern_associated_strings preferRootPartial);
-  let checkPref extract (pref, prefName) =
-    try
-      let root =
-        List.find (fun r -> not (List.mem r (Globals.rawRoots ())))
-          (extract pref)
-      in
-      let (r1, r2) = Globals.rawRootPair () in
-      raise (Util.Fatal (Printf.sprintf
-        "%s (given as argument to '%s' preference)\n\
-         is not one of the current roots:\n  %s\n  %s" root prefName r1 r2))
-    with Not_found ->
-      ()
+            "The '%s=older' preference can only be used with 'times=true'"
+            predname))
+    | `Merge -> assert false
+    | exception (Util.Fatal err) ->
+        raise (Util.Fatal ("Argument to preference '" ^ predname ^ "': " ^ err))
+  in
+  let checkPrefs ~explicitRoot extract prefs =
+    Safelist.iter (fun (pref, prefName) ->
+      Safelist.iter (test_root explicitRoot prefName) (extract pref)) prefs
   in
-  List.iter (checkPref Prefs.read)
+  checkPrefs ~explicitRoot:false (fun x -> [Prefs.read x])
+    [forceRoot, "force"; preferRoot, "prefer"];
+  checkPrefs ~explicitRoot:false Pred.extern_associated_strings
+    [forceRootPartial, "forcepartial";
+     preferRootPartial, "preferpartial"];
+  checkPrefs ~explicitRoot:true Prefs.read
     [noDeletion, "nodeletion"; noUpdate, "noupdate"; noCreation, "nocreation"];
-  List.iter (checkPref Pred.extern_associated_strings)
+  checkPrefs ~explicitRoot:true Pred.extern_associated_strings
     [noDeletionPartial, "nodeletionpartial";
      noUpdatePartial, "noupdatepartial";
      noCreationPartial, "nocreationpartial"]
@@ -349,7 +408,7 @@ let rec checkForError ui =
     NoUpdates ->
       ()
   | Error err ->
-      raise (UpdateError err)
+      if not (Fileinfo.shouldIgnore err) then raise (UpdateError err)
   | Updates (uc, _) ->
       match uc with
         Dir (_, children, _, _) ->
@@ -362,7 +421,7 @@ let rec collectErrors ui rem =
     NoUpdates ->
       rem
   | Error err ->
-      err :: rem
+      if Fileinfo.shouldIgnore err then rem else err :: rem
   | Updates (uc, _) ->
       match uc with
         Dir (_, children, _, _) ->
@@ -391,6 +450,22 @@ let propagateErrors allowPartial (rplc: Common.replicas): Common.replicas =
       with UpdateError err ->
         Problem ("[root 1]: " ^ err)
 
+(* Using the error message to ignore symlinks is a bit fragile but this is
+   the easiest way to keep code changes local and avoid a huge backwards
+   compatibility burden. *)
+
+let skipIgnored result s othUi =
+  match Fileinfo.shouldIgnore s, othUi with
+  | false, _ -> Tree.add result (Problem s)
+  | true, Error s2 ->
+      if Fileinfo.shouldIgnore s2 then result else Tree.add result (Problem s2)
+  | true, NoUpdates
+  | true, Updates (Symlink _, _) -> result
+  | true, Updates _ ->
+      Tree.add result (Problem "Syncing symbolic links is disabled, but \
+        this path represents a symbolic link in one of the replicas and \
+        a non-link in the other replica.")
+
 type singleUpdate = Rep1Updated | Rep2Updated
 
 let update2replicaContent path (conflict: bool) ui props ucNew oldType:
@@ -477,7 +552,7 @@ let rec reconcileNoConflict allowPartial path props' ui props whatIsUpdated
   match ui with
   | NoUpdates -> result
   | Error err ->
-      Tree.add result (Problem err)
+      skipIgnored result err NoUpdates
   | Updates (Dir (desc, children, permchg, _),
              Previous(`DIRECTORY, _, _, _)) ->
       let r =
@@ -566,9 +641,9 @@ let rec reconcile
                       errors1 = []; errors2 = []}))) in
   match (ui1, ui2) with
     (Error s, _) ->
-      (equals, Tree.add unequals (Problem s))
+      (equals, skipIgnored unequals s ui2)
   | (_, Error s) ->
-      (equals, Tree.add unequals (Problem s))
+      (equals, skipIgnored unequals s ui1)
   | (NoUpdates, _)  ->
       (equals,
        reconcileNoConflict
@@ -642,6 +717,9 @@ let rec reconcile
                        (oldType prev) equals unequals
        | ContentsSame, ContentsSame when Props.similar desc1 desc2 ->
            (add_equal counter equals (uc1, uc2), unequals)
+       | ContentsSame, ContentsSame ->
+           different uc1 uc2 "properties changed on both sides"
+                     (oldType prev) equals unequals
        | ContentsUpdated _, ContentsUpdated _
              when Globals.shouldMerge path ->
            toBeMerged uc1 uc2 (oldType prev) equals unequals
diff --git a/src/remote.ml b/src/remote.ml
index b2ff84f..d101c39 100644
--- a/src/remote.ml
+++ b/src/remote.ml
@@ -81,23 +81,53 @@ let decodeInt int_buf i =
 (*                           LOW-LEVEL IO                                *)
 (*************************************************************************)
 
-let lostConnection () =
+let ioCleanups = ref []
+
+let registerIOClose io f =
+  ioCleanups := (io, f) :: !ioCleanups
+
+let lostConnectionHandler ch =
+  let aux ((i, o), f) =
+    if i = ch || o = ch then begin
+      f ();
+      false (* Each handler is run only once *)
+    end else
+      true
+  in
+  ioCleanups := Safelist.filter aux !ioCleanups
+
+let lostConnection ch =
+  begin try lostConnectionHandler ch with _ -> () end;
   Lwt.fail (Util.Fatal "Lost connection with the server")
 
-let catchIoErrors th =
+let catchIoErrors ch th =
   Lwt.catch th
     (fun e ->
        match e with
          Unix.Unix_error(Unix.ECONNRESET, _, _)
        | Unix.Unix_error(Unix.EPIPE, _, _)
+       | Unix.Unix_error(Unix.ETIMEDOUT, _, _)
+       | Unix.Unix_error(Unix.EACCES, _, _)        (* Linux firewall *)
          (* Windows may also return the following errors... *)
-       | Unix.Unix_error(Unix.EINVAL, _, _)
+       | Unix.Unix_error(Unix.EINVAL, _, _) (* ... and Linux firewall *)
        | Unix.Unix_error(Unix.EUNKNOWNERR (-64), _, _)
                          (* ERROR_NETNAME_DELETED *)
-       | Unix.Unix_error(Unix.EUNKNOWNERR (-233), _, _) ->
+       | Unix.Unix_error(Unix.EUNKNOWNERR (-233), _, _)
                          (* ERROR_PIPE_NOT_CONNECTED *)
+       | Unix.Unix_error(Unix.EUNKNOWNERR (-1236), _, _)
+                         (* ERROR_CONNECTION_ABORTED *)
+         (* The following errors _may_ be temporary but we don't know if
+            they are or for how long they will persist. We also don't have
+            a way to retry and there is no guarantee that the socket remains
+            in a usable state, so treat all these as permanent failures
+            breaking the connection. *)
+       | Unix.Unix_error(Unix.ENETUNREACH, _, _)
+       | Unix.Unix_error(Unix.EHOSTUNREACH, _, _)
+       | Unix.Unix_error(Unix.ENETDOWN, _, _)
+       | Unix.Unix_error(Unix.EHOSTDOWN, _, _)
+       | Unix.Unix_error(Unix.ENETRESET, _, _) ->
          (* Client has closed its end of the connection *)
-           lostConnection ()
+           lostConnection ch
        | _ ->
            Lwt.fail e)
 
@@ -130,7 +160,7 @@ let makeBuffer ch =
 
 let fillInputBuffer conn =
   assert (conn.length = 0);
-  catchIoErrors
+  catchIoErrors conn.channel
     (fun () ->
        Lwt_unix.read conn.channel conn.buffer 0 bufferSize >>= fun len ->
        debugV (fun() ->
@@ -140,7 +170,7 @@ let fillInputBuffer conn =
            Util.msg "grab: %s\n"
              (String.escaped (Bytes.sub_string conn.buffer 0 len)));
        if len = 0 then
-         lostConnection ()
+         lostConnection conn.channel
        else begin
          receivedBytes := !receivedBytes +. float len;
          conn.length <- len;
@@ -171,12 +201,19 @@ let grab conn s len =
 let peekWithoutBlocking conn =
   Bytes.sub conn.buffer 0 conn.length
 
+let peekWithBlocking conn =
+  (if conn.length = 0 then begin
+    fillInputBuffer conn
+  end else
+    Lwt.return ()) >>= fun () ->
+  Lwt.return (peekWithoutBlocking conn)
+
 (****)
 
 (* Low-level outputs *)
 
 let rec sendOutput conn =
-  catchIoErrors
+  catchIoErrors conn.channel
     (fun () ->
        begin if conn.opened then
          Lwt_unix.write conn.channel conn.buffer 0 conn.length
@@ -289,16 +326,44 @@ let disableFlowControl q =
 
 let outputQueueIsEmpty q = q.available
 
+(* Setup IO with flow control initially disabled, to do the RPC version
+   handshake. Flow control is part of RPC protocol and must be enabled
+   only after RPC version handshake is complete. *)
 let makeOutputQueue isServer flush =
-  { available = true; canWrite = isServer; flowControl = true;
+  { available = true; canWrite = true; flowControl = false;
     writes = Queue.create (); urgentWrites = Queue.create ();
     idleWrites = Queue.create ();
     flush = flush }
 
 (****)
 
+(* IMPORTANT: the RPC version must be increased when the RPC mechanism itself
+   changes in a breaking way. Changes on the API level (functions and data
+   types) normally do not cause a breaking change at the RPC level. *)
+(* Version 0 is special in that it must not be listed as a supported version.
+   It is used for 2.51-compatibility mode and is never negotiated. *)
+(* Supported RPC versions should be ordered from newest to oldest. *)
+let rpcSupportedVersions = [1]
+let rpcDefaultVersion = Safelist.hd rpcSupportedVersions
+
+let rpcSupportedVersionStr =
+  String.concat ", "
+    (Safelist.map (fun v -> "\"" ^ string_of_int v ^ "\"")
+       rpcSupportedVersions)
+
+let rpcSupportedVersionStrHdr =
+  String.concat " "
+    (Safelist.map (fun v -> string_of_int v)
+       rpcSupportedVersions)
+
+(* FIX: Added in 2021. Should be removed after a couple of years. *)
+let rpcServerCmdlineOverride = "__new-rpc-mode"
+
+(****)
+
 type connection =
-  { inputBuffer : ioBuffer;
+  { mutable version : int;
+    inputBuffer : ioBuffer;
     outputBuffer : ioBuffer;
     outputQueue : outputQueue }
 
@@ -335,12 +400,32 @@ let maybeFlush pendingFlush q buf =
 let makeConnection isServer inCh outCh =
   let pendingFlush = ref false in
   let outputBuffer = makeBuffer outCh in
-  { inputBuffer = makeBuffer inCh;
+  { version = rpcDefaultVersion;
+    inputBuffer = makeBuffer inCh;
     outputBuffer = outputBuffer;
     outputQueue =
       makeOutputQueue isServer
         (fun q -> maybeFlush pendingFlush q outputBuffer) }
 
+let closeConnection conn =
+  begin try Lwt_unix.close conn.inputBuffer.channel with Unix.Unix_error _ -> () end;
+  begin try Lwt_unix.close conn.outputBuffer.channel with Unix.Unix_error _ -> () end;
+  conn.outputBuffer.opened <- false
+
+let connectionIO conn =
+  (conn.inputBuffer.channel, conn.outputBuffer.channel)
+
+let setConnectionVersion conn ver =
+  conn.version <- ver
+
+let connectionVersion conn = conn.version
+
+let connEq conn conn' =
+  conn.inputBuffer.channel = conn'.inputBuffer.channel
+    && conn.outputBuffer.channel = conn'.outputBuffer.channel
+
+let connNeq conn conn' = not (connEq conn conn')
+
 (* Send message [l] *)
 let dump conn l =
   performOutput
@@ -361,11 +446,204 @@ let dumpUrgent conn l =
        fillBuffer conn.outputBuffer l >>= fun () ->
        flushBuffer conn.outputBuffer)
 
+let enableFlowControl conn isServer =
+  let rec waitDrain () =
+    if not isServer && conn.outputBuffer.length > 0 then
+      Lwt_unix.yield () >>= waitDrain
+    else
+      Lwt.return ()
+  in
+  let q = conn.outputQueue in
+  q.available <- false;
+  waitDrain () >>= fun () ->
+  q.flowControl <- true;
+  q.canWrite <- isServer;
+  if q.canWrite then
+    popOutputQueues q >>= Lwt_unix.yield >>= fun () ->
+    Lwt.return ()
+  else
+    Lwt.return ()
+
+(****)
+
+let connectionCheck = ref None
+
+let checkConnection ioServer =
+  connectionCheck := Some ioServer;
+  (* Poke on the socket to trigger an error if connection has been lost. *)
+  Lwt_unix.run (
+    (if (Util.osType = `Win32) then Lwt.return 0 else
+    Lwt_unix.read ioServer.inputBuffer.channel ioServer.inputBuffer.buffer 0 0)
+    (* Try to make sure connection cleanup, if necessary, has finished
+       before returning.
+       Since there is no way to reliably detect when other threads have
+       finished, we just yield a bit (the same comments apply as in
+       commandLoop). *)
+    >>= fun _ ->
+    let rec wait n =
+      if n = 0 then Lwt.return () else begin
+        Lwt_unix.yield () >>= fun () ->
+        wait (n - 1)
+      end
+    in
+    wait 10);
+  connectionCheck := None
+
+let isConnectionCheck conn =
+  match !connectionCheck with
+  | None -> false
+  | Some conn' -> connEq conn conn'
+
+(* Due to [Common.root] currently excluding important details present in
+   [Clroot.clroot], there is a 1:N mapping between a [root] and a [clroot].
+   For example, a [clroot] pointing to the same host but a different
+   protocol or user or port will be mapped to the same [root] as long as
+   the root fspaths are the same.
+   It is currently (Oct 2022) not seen as a critical issue to fix.
+   The code previously used to index connections just by the canonical host
+   name, ignoring all other details, including the root fspath. That code
+   was in place for over 20 years and did not seem to cause any issues.
+   The current code is safer than the previous code... *)
+module ClientConn = struct
+  type t =
+    { clroot : Clroot.clroot;
+      root : Common.root;
+      conn : connection }
+      (* Never do polymorphic comparisons with [connection]! *)
+
+  let connections = ref []
+
+  let findByClroot clroot = Safelist.find (fun x -> x.clroot = clroot) !connections
+  let findByRoot root = Safelist.find (fun x -> x.root = root) !connections
+
+  let register clroot root conn =
+    connections := { clroot; root; conn } ::
+      Safelist.filter (fun x -> x.clroot <> clroot) !connections
+
+  let unregister conn =
+    connections := Safelist.filter (fun x -> connNeq x.conn conn) !connections
+
+  let ofRoot root =
+    try (findByRoot root).conn with
+    | Not_found -> raise (Util.Fatal "No connection with the server")
+
+  let ofRootOpt root =
+    try Some (findByRoot root).conn with
+    | Not_found -> None
+
+  let canonRootOfClroot clroot =
+    try Some (findByClroot clroot).root with
+    | Not_found -> None
+
+  let withConncheck find =
+    try
+      let conn = (find ()).conn in
+      begin try
+        checkConnection conn
+      with
+      | Unix.Unix_error (EBADF, _, _) -> (* Already closed *)
+          unregister conn
+        (* (All or most?) other exceptions should be caught by receiving and
+           sending threads. If this does not happen (it also depends on the
+           implementation of [Lwt_unix.run]) then trigger the cleanup here as
+           the last resort. *)
+      | Unix.Unix_error _ ->
+          try
+            lostConnection (fst (connectionIO conn)) |> ignore;
+            unregister conn
+          with e -> begin
+            unregister conn;
+            raise e
+          end
+      end;
+      (* [find] _must_ be duplicated after [checkConnection]! *)
+      Some (find ()).conn
+    with Not_found ->
+      None
+
+  let ofRootConncheck root =
+    withConncheck (fun () -> findByRoot root)
+
+  let ofClrootConncheck clroot =
+    withConncheck (fun () -> findByClroot clroot)
+
+end (* module ClientConn *)
+
+let connectionOfRoot root = ClientConn.ofRoot root
+
+(****)
+
+let atCloseHandlers = ref []
+
+let at_conn_close ?(only_server = false) f =
+  atCloseHandlers := (only_server, f) :: !atCloseHandlers
+
+let runConnCloseHandlers isServer =
+  Safelist.iter (fun (only_server, f) ->
+    if not only_server || isServer then f ()) !atCloseHandlers
+
+let atConnCloseHandlers = ref []
+
+let at_conn_close' conn f =
+  atConnCloseHandlers := (conn, f) :: !atConnCloseHandlers
+
+let runConnCloseHandlers' conn =
+  atConnCloseHandlers := Safelist.filter (fun (c, f) ->
+    if connEq c conn then (f (); false) else true) !atConnCloseHandlers
+
+let clientCloseCleanup () =
+  runConnCloseHandlers false
+
+let clientConnClose conn =
+  closeConnection conn;
+  ClientConn.unregister conn;
+  runConnCloseHandlers' conn;
+  clientCloseCleanup ()
+
+let registerConnCleanup conn cleanup =
+  registerIOClose (connectionIO conn) (fun () -> clientConnClose conn);
+  match cleanup with
+  | None -> ()
+  | Some f -> at_conn_close' conn f
+
+let clientCloseRootConnection = function
+  | (Common.Local, _) -> clientCloseCleanup ()
+  | (Common.Remote _, _) as root ->
+      begin match ClientConn.ofRootOpt root with
+      | Some conn -> clientConnClose conn
+      | None -> ()
+      end
+
 (****)
 
-(* Initialize the connection *)
-let setupIO isServer inCh outCh =
-  makeConnection isServer inCh outCh
+(* Implemented as a record to avoid polluting [Remote] namespace. If
+   the number and complexity of functions grows in future then it's
+   probably a good idea to extract this code into a separate module. *)
+type ('a, 'b, 'c) resourceC =
+  { register : 'a -> 'a; release : 'a -> 'b; release_noerr : 'a -> 'c }
+let resourceWithConnCleanup close close_noerr =
+  let h = Hashtbl.create 17 in
+  let closeAll () =
+    Hashtbl.iter (fun x _ -> ignore (close_noerr x)) h;
+    Hashtbl.clear h
+  in
+  at_conn_close closeAll;
+  let register x = Hashtbl.add h x true; x in
+  let release x = Hashtbl.remove h x; close x in
+  let release_noerr x = Hashtbl.remove h x; close_noerr x in
+  { register; release; release_noerr }
+
+let lwtRegionWithConnCleanup sz =
+  let reg = ref (Lwt_util.make_region sz) in
+  let resetReg () =
+    Lwt_util.purge_region !reg;
+    (* The remaining threads should be collected by GC *)
+    reg := Lwt_util.make_region sz
+  in
+  at_conn_close resetReg;
+  reg
+
+(****)
 
 (* XXX *)
 module Thread = struct
@@ -394,11 +672,25 @@ end
 
 type tag = Bytearray.t
 
-type 'a marshalFunction =
+type 'a marshalFunction = connection ->
   'a -> (Bytearray.t * int * int) list -> (Bytearray.t * int * int) list
-type 'a unmarshalFunction = Bytearray.t -> 'a
+type 'a unmarshalFunction = connection -> Bytearray.t -> 'a
 type 'a marshalingFunctions = 'a marshalFunction * 'a unmarshalFunction
 
+type 'a convV0Fun =
+  V0 : ('a -> 'compat) * ('compat -> 'a) -> 'a convV0Fun [@unboxed]
+
+external id : 'a -> 'a = "%identity"
+let convV0_id = V0 (id, id)
+let convV0_id_pair = convV0_id, convV0_id
+
+let makeConvV0FunArg compat_to compat_from =
+  (V0 (compat_to, compat_from)), convV0_id
+let makeConvV0FunRet compat_to compat_from =
+  convV0_id, (V0 (compat_to, compat_from))
+let makeConvV0Funs compat_to compat_from compat_to2 compat_from2 =
+  (V0 (compat_to, compat_from)), (V0 (compat_to2, compat_from2))
+
 let registeredSet = ref Util.StringSet.empty
 
 let rec first_chars len msg =
@@ -420,7 +712,18 @@ let safeMarshal marshalPayload tag data rem =
             let start = String.escaped start in
             Util.msg "send [%s] '%s' %d bytes\n"
               (Bytearray.to_string tag) start length);
-  (encodeInt (l + length) :: (tag, 0, l) :: rem')
+  let len = l + length in
+  if (len lsr 31) lsr 1 <> 0 then (* [encodeInt] can only encode 32 bits *)
+    raise (Util.Fatal
+            "Protocol error: message data too big. This may be a bug or it\n\
+             may be that your replicas are huge and the amount of updates\n\
+             can't be handled by the current protocol implementation. If you\n\
+             believe it is a bug then please consider reporting it.\n\
+             Otherwise, try reducing the amount of updates by syncing the\n\
+             replicas in smaller steps (using the \"path\" preference, for\n\
+             example). You may have to do this for the initial sync only.")
+  else
+    (encodeInt len :: (tag, 0, l) :: rem')
 
 let safeUnmarshal unmarshalPayload tag buf =
   let taglength = Bytearray.length tag in
@@ -441,23 +744,37 @@ let registerTag string =
     registeredSet := Util.StringSet.add string !registeredSet;
   Bytearray.of_string string
 
-let defaultMarshalingFunctions =
-  (fun data rem ->
-     let s = Bytearray.marshal data [Marshal.No_sharing] in
-     let l = Bytearray.length s in
-     ((s, 0, l) :: rem, l)),
-  (fun buf pos ->
-      try Bytearray.unmarshal buf pos
-      with Failure s -> raise (Util.Fatal (Printf.sprintf 
+let marshalV0 (V0 (to251, _)) data rem =
+  let s = Bytearray.marshal (to251 data) [Marshal.No_sharing] in
+  let l = Bytearray.length s in
+  ((s, 0, l) :: rem, l)
+
+let unmarshalV0 (V0 (_, from251)) buf pos =
+  try from251 (Bytearray.unmarshal buf pos)
+  with Failure s -> raise (Util.Fatal (Printf.sprintf
 "Fatal error during unmarshaling (%s),
 possibly because client and server have been compiled with different \
-versions of the OCaml compiler." s)))
+versions of the OCaml compiler." s))
+
+let marshalV1 m data rem =
+  let s = Umarshal.marshal_to_bytearray m data in
+  let l = Bytearray.length s in
+  ((s, 0, l) :: rem, l)
+
+let unmarshalV1 m buf pos =
+  try Umarshal.unmarshal_from_bytearray m buf pos
+  with Failure s | Umarshal.Error s -> raise (Util.Fatal (Printf.sprintf
+"Fatal error during unmarshaling (%s)" s))
+
+let defaultMarshalingFunctions convV0 m =
+  (fun conn -> if conn.version = 0 then marshalV0 convV0 else marshalV1 m),
+  (fun conn -> if conn.version = 0 then unmarshalV0 convV0 else unmarshalV1 m)
 
 let makeMarshalingFunctions payloadMarshalingFunctions string =
   let (marshalPayload, unmarshalPayload) = payloadMarshalingFunctions in
   let tag = registerTag string in
-  let marshal (data : 'a) rem = safeMarshal marshalPayload tag data rem in
-  let unmarshal buf = (safeUnmarshal unmarshalPayload tag buf : 'a) in
+  let marshal conn (data : 'a) rem = safeMarshal (marshalPayload conn) tag data rem in
+  let unmarshal conn buf = (safeUnmarshal (unmarshalPayload conn) tag buf : 'a) in
   (marshal, unmarshal)
 
 (*****************************************************************************)
@@ -468,29 +785,15 @@ let makeMarshalingFunctions payloadMarshalingFunctions string =
    these be part of it too? *)
 let sshCmd =
   Prefs.createString "sshcmd" "ssh"
-    ("!path to the ssh executable")
+    ~category:(`Advanced `Remote)
+    ("path to the ssh executable")
     ("This preference can be used to explicitly set the name of the "
      ^ "ssh executable (e.g., giving a full path name), if necessary.")
 
-let rshCmd =
-  Prefs.createString "rshcmd" "rsh"
-    ("*path to the rsh executable")
-    ("This preference can be used to explicitly set the name of the "
-     ^ "rsh executable (e.g., giving a full path name), if necessary.")
-
-let rshargs =
-  Prefs.createString "rshargs" ""
-    "*other arguments (if any) for remote shell command"
-    ("The string value of this preference will be passed as additional "
-     ^ "arguments (besides the host name and the name of the Unison "
-     ^ "executable on the remote system) to the \\verb|rsh| "
-     ^ "command used to invoke the remote server. The backslash is an "
-     ^ "escape character."
-     )
-
 let sshargs =
   Prefs.createString "sshargs" ""
-    "!other arguments (if any) for remote shell command"
+    ~category:(`Advanced `Remote)
+    "other arguments (if any) for remote shell command"
     ("The string value of this preference will be passed as additional "
      ^ "arguments (besides the host name and the name of the Unison "
      ^ "executable on the remote system) to the \\verb|ssh| "
@@ -498,16 +801,22 @@ let sshargs =
      ^ "escape character."
      )
 
+(* rsh prefs removed since 2.52 *)
+let () = Prefs.markRemoved "rshcmd"
+let () = Prefs.markRemoved "rshargs"
+
 let serverCmd =
   Prefs.createString "servercmd" ""
-    ("!name of " ^ Uutil.myName ^ " executable on remote server")
+    ~category:(`Advanced `Remote)
+    ("name of " ^ Uutil.myName ^ " executable on remote server")
     ("This preference can be used to explicitly set the name of the "
      ^ "Unison executable on the remote server (e.g., giving a full "
      ^ "path name), if necessary.")
 
 let addversionno =
   Prefs.createBool "addversionno" false
-    ("!add version number to name of " ^ Uutil.myName ^ " on server")
+    ~category:(`Advanced `Remote)
+    ("add version number to name of " ^ Uutil.myName ^ " on server")
     ("When this flag is set to {\\tt true}, Unison "
      ^ "will use \\texttt{unison-\\ARG{currentmajorversionnumber}} instead of "
      ^ "just \\verb|unison| as the remote server command (note that the minor "
@@ -516,28 +825,6 @@ let addversionno =
      ^ "conveniently on the same server: whichever version is run "
      ^ "on the client, the same version will be selected on the server.")
 
-(* List containing the connected hosts and the file descriptors of
-   the communication. *)
-let connectionsByHosts = ref []
-
-(* Gets the Read/Write file descriptors for a host;
-   the connection must have been set up by canonizeRoot before calling *)
-let hostConnection host =
-  try Safelist.assoc host !connectionsByHosts
-  with Not_found ->
-    raise(Util.Fatal "Remote.hostConnection")
-
-(* connectedHosts is a list of command-line roots and their corresponding
-   canonical host names.
-   Local command-line roots are not in the list.
-   Although there can only be one remote host per sync, it's possible
-   connectedHosts to hold more than one hosts if more than one sync is
-   performed.
-   It's also possible for there to be two connections open for the
-   same canonical root.
-*)
-let connectedHosts = ref []
-
 (**********************************************************************
                        CLIENT/SERVER PROTOCOLS
  **********************************************************************)
@@ -613,8 +900,24 @@ type header =
   | Stream of string
   | StreamAbort
 
+let mheader = Umarshal.(sum6 unit string string string string unit
+                          (function
+                           | NormalResult -> I61 ()
+                           | TransientExn a -> I62 a
+                           | FatalExn a -> I63 a
+                           | Request a -> I64 a
+                           | Stream a -> I65 a
+                           | StreamAbort -> I66 ())
+                          (function
+                           | I61 () -> NormalResult
+                           | I62 a -> TransientExn a
+                           | I63 a -> FatalExn a
+                           | I64 a -> Request a
+                           | I65 a -> Stream a
+                           | I66 () -> StreamAbort))
+
 let ((marshalHeader, unmarshalHeader) : header marshalingFunctions) =
-  makeMarshalingFunctions defaultMarshalingFunctions "rsp"
+  makeMarshalingFunctions (defaultMarshalingFunctions convV0_id mheader) "rsp"
 
 let processRequest conn id cmdName buf =
   let cmd =
@@ -624,28 +927,46 @@ let processRequest conn id cmdName buf =
   Lwt.try_bind (fun () -> cmd conn buf)
     (fun marshal ->
        debugE (fun () -> Util.msg "Sending result (id: %d)\n" (decodeInt id 0));
-       dump conn ((id, 0, intSize) :: marshalHeader NormalResult (marshal [])))
+       dump conn ((id, 0, intSize) :: marshalHeader conn NormalResult (marshal [])))
     (function
        Util.Transient s ->
          debugE (fun () ->
            Util.msg "Sending transient exception (id: %d)\n" (decodeInt id 0));
-         dump conn ((id, 0, intSize) :: marshalHeader (TransientExn s) [])
+         dump conn ((id, 0, intSize) :: marshalHeader conn (TransientExn s) [])
      | Util.Fatal s ->
          debugE (fun () ->
            Util.msg "Sending fatal exception (id: %d)\n" (decodeInt id 0));
-         dump conn ((id, 0, intSize) :: marshalHeader (FatalExn s) [])
+         dump conn ((id, 0, intSize) :: marshalHeader conn (FatalExn s) [])
      | e ->
          Lwt.fail e)
+  (* With the current RPC protocol it is not possible to recover from situations
+     where an RPC packet is not completely transmitted (due to an interrupted
+     write syscall). The other side will hang forever, waiting for the complete
+     packet to arrive. New packets (here, transmitting the exception) can't be
+     sent because the receiver can't read them until the previous packet is
+     complete. (Best case, the server quits with a protocol error.)
+
+     Therefore, it is important that exceptions that can interrupt syscalls
+     (for example, Sys.Break (Ctrl-C)) are never wrapped into Util.Transient or
+     Util.Fatal, unless the connection is already known to be broken. Likewise,
+     other exception handlers than the one just above must avoid writing out
+     additional data to the RPC connection. *)
 
 let streamAbortedSrc = ref 0
 let streamAbortedDst = ref false
 
 let streamError = Hashtbl.create 7
 
+let resetStreamErroState () =
+  streamAbortedSrc := 0;
+  streamAbortedDst := false;
+  Hashtbl.reset streamError
+let () = at_conn_close resetStreamErroState
+
 let abortStream conn id =
   if not !streamAbortedDst then begin
     streamAbortedDst := true;
-    let request = encodeInt id :: marshalHeader StreamAbort [] in
+    let request = encodeInt id :: marshalHeader conn StreamAbort [] in
     dumpUrgent conn request
   end else
     Lwt.return ()
@@ -679,6 +1000,9 @@ let newMsgId () = incr ids; if !ids = hugeint then ids := 2; !ids
 
 (* Threads waiting for a response from the other side *)
 let receivers = ref MsgIdMap.empty
+let resetReceivers () =
+  receivers := MsgIdMap.empty
+let () = at_conn_close resetReceivers
 
 let find_receiver id =
   let thr = MsgIdMap.find id !receivers in
@@ -703,7 +1027,7 @@ let rec receive conn =
         (fun () -> Util.msg "Message received (id: %d)\n" num_id);
       (* Read the header *)
       receivePacket conn >>= (fun buf ->
-      let req = unmarshalHeader buf in
+      let req = unmarshalHeader conn buf in
       begin match req with
         Request cmdName ->
           receivePacket conn >>= (fun buf ->
@@ -764,9 +1088,9 @@ let registerSpecialServerCmd
     makeMarshalingFunctions marshalingFunctionsResult (cmdName ^ "-res") in
   (* Create a server function and remember it *)
   let server conn buf =
-    let args = unmarshalArgs buf in
+    let args = unmarshalArgs conn buf in
     serverSide conn args >>= (fun answer ->
-    Lwt.return (marshalResult answer))
+    Lwt.return (marshalResult conn answer))
   in
   serverCmds := Util.StringMap.add cmdName server !serverCmds;
   (* Create a client function and return it *)
@@ -775,19 +1099,20 @@ let registerSpecialServerCmd
     assert (id >= 0); (* tracking down an assert failure in receivePacket... *)
     let request =
       encodeInt id ::
-      marshalHeader (Request cmdName) (marshalArgs serverArgs [])
+      marshalHeader conn (Request cmdName) (marshalArgs conn serverArgs [])
     in
     let reply = wait_for_reply id in
     debugE (fun () -> Util.msg "Sending request (id: %d)\n" id);
     dump conn request >>= (fun () ->
     reply >>= (fun buf ->
-    Lwt.return (unmarshalResult buf)))
+    Lwt.return (unmarshalResult conn buf)))
   in
   client
 
-let registerServerCmd name f =
+let registerServerCmd name ?(convV0=convV0_id_pair) mArg mRet f =
   registerSpecialServerCmd
-    name defaultMarshalingFunctions defaultMarshalingFunctions f
+    name (defaultMarshalingFunctions (fst convV0) mArg)
+         (defaultMarshalingFunctions (snd convV0) mRet) f
 
 (* RegisterHostCmd is a simpler version of registerClientServer [registerServerCmd?].
    It is used to create remote procedure calls: the only communication
@@ -799,49 +1124,47 @@ let registerServerCmd name f =
    RegisterHostCmd recognizes the case where the server is the local
    host, and it avoids socket communication in this case.
 *)
-let registerHostCmd cmdName cmd =
+let registerHostCmd cmdName ?(convV0=convV0_id_pair) mArg mRet cmd =
   let serverSide = (fun _ args -> cmd args) in
   let client0 =
-    registerServerCmd cmdName serverSide in
-  let client host args =
-    let conn = hostConnection host in
+    registerServerCmd cmdName ~convV0 mArg mRet serverSide in
+  let client root args =
+    let conn = ClientConn.ofRoot root in
     client0 conn args in
   (* Return a function that runs either the proxy or the local version,
      depending on whether the call is to the local host or a remote one *)
-  fun host args ->
-    match host with
-      "" -> cmd args
-    | _  -> client host args
-
-let hostOfRoot root =
-  match root with
-    (Common.Local, _)       -> ""
-  | (Common.Remote host, _) -> host
-let connectionToRoot root = hostConnection (hostOfRoot root)
+  fun root args ->
+    match root with
+    | (Common.Local, _) -> cmd args
+    | (Common.Remote _, _) -> client root args
 
 (* RegisterRootCmd is like registerHostCmd but it indexes connections by
    root instead of host. *)
-let registerRootCmd (cmdName : string) (cmd : (Fspath.t * 'a) -> 'b) =
-  let r = registerHostCmd cmdName cmd in
-  fun root args -> r (hostOfRoot root) ((snd root), args)
-
-let registerRootCmdWithConnection
-  (cmdName : string) (cmd : connection -> 'a -> 'b) =
-  let client0 = registerServerCmd cmdName cmd in
+let registerRootCmd (cmdName : string)
+  ?(convV0=convV0_id_pair) mArg mRet (cmd : (Fspath.t * 'a) -> 'b) =
+  let mArg = Umarshal.(prod2 Fspath.m mArg id id) in
+  let r = registerHostCmd cmdName ~convV0 mArg mRet cmd in
+  fun root args -> r root ((snd root), args)
+
+let registerRootCmdWithConnection (cmdName : string)
+  ?(convV0=convV0_id_pair) mArg mRet (cmd : connection -> 'a -> 'b) =
+  let client0 = registerServerCmd cmdName ~convV0 mArg mRet cmd in
   (* Return a function that runs either the proxy or the local version,
      depending on whether the call is to the local host or a remote one *)
   fun localRoot remoteRoot args ->
-    match (hostOfRoot localRoot) with
-      "" -> let conn = hostConnection (hostOfRoot remoteRoot) in
+    match (fst localRoot) with
+    | Common.Local -> let conn = ClientConn.ofRoot remoteRoot in
             cmd conn args
-    | _  -> let conn = hostConnection (hostOfRoot localRoot) in
+    | _  -> let conn = ClientConn.ofRoot localRoot in
             client0 conn args
 
-let streamReg = Lwt_util.make_region 1
+let streamReg = lwtRegionWithConnCleanup 1
 
 let streamingActivated =
   Prefs.createBool "stream" true
-    ("!use a streaming protocol for transferring file contents")
+    ~category:(`Advanced `Remote)
+    ~deprecated:true
+    ("use a streaming protocol for transferring file contents")
     "When this preference is set, Unison will use an experimental \
      streaming protocol for transferring file contents more efficiently. \
      The default value is \\texttt{true}."
@@ -853,11 +1176,12 @@ let registerStreamCmd
     =
   let cmd =
     registerSpecialServerCmd
-      cmdName marshalingFunctionsArgs defaultMarshalingFunctions
+      cmdName marshalingFunctionsArgs
+      (defaultMarshalingFunctions convV0_id Umarshal.unit)
       (fun conn v -> serverSide conn v; Lwt.return ())
   in
   let ping =
-    registerServerCmd (cmdName ^ "Ping")
+    registerServerCmd (cmdName ^ "Ping") Umarshal.int Umarshal.unit
       (fun conn (id : int) ->
          try
            let e = Hashtbl.find streamError id in
@@ -875,7 +1199,7 @@ let registerStreamCmd
     makeMarshalingFunctions marshalingFunctionsArgs (cmdName ^ "-str") in
   (* Create a server function and remember it *)
   let server conn buf =
-    let args = unmarshalArgs buf in
+    let args = unmarshalArgs conn buf in
     serverSide conn args
   in
   serverStreams := Util.StringMap.add cmdName server !serverStreams;
@@ -885,7 +1209,7 @@ let registerStreamCmd
     if !streamAbortedSrc = id then raise (Util.Transient "Streaming aborted");
     let request =
       encodeInt id ::
-      marshalHeader (Stream cmdName) (marshalArgs serverArgs [])
+      marshalHeader conn (Stream cmdName) (marshalArgs conn serverArgs [])
     in
     dumpIdle conn request
   in
@@ -897,46 +1221,241 @@ let registerStreamCmd
       let id = newMsgId () in (* Message ID *)
       Lwt.try_bind
         (fun () ->
-           Lwt_util.run_in_region streamReg 1
+           Lwt_util.run_in_region !streamReg 1
              (fun () -> sender (fun v -> client conn id v)))
         (fun v -> ping conn id >>= fun () -> Lwt.return v)
         (fun e ->
-           debugE (fun () ->
-             Util.msg "Pinging remote end after streaming error\n");
-           ping conn id >>= fun () -> Lwt.fail e)
+           if !streamAbortedSrc = id then begin
+             debugE (fun () ->
+               Util.msg "Pinging remote end after streaming error\n");
+             ping conn id >>= fun () -> Lwt.fail e
+           end else
+             Lwt.fail e)
     end
 
 let commandAvailable =
-  registerRootCmd "commandAvailable"
+  registerRootCmd "commandAvailable" Umarshal.string Umarshal.bool
     (fun (_, cmdName) -> Lwt.return (Util.StringMap.mem cmdName !serverCmds))
 
 (****************************************************************************
                      BUILDING CONNECTIONS TO THE SERVER
  ****************************************************************************)
 
-let connectionHeader =
-  let (major,minor,patchlevel) =
-    Scanf.sscanf Sys.ocaml_version "%d.%d.%d" (fun x y z -> (x,y,z)) in
-  let compiler =
-    if    major < 4 
-       || major = 4 && minor < 2
-       || major = 4 && minor = 2 && patchlevel <= 1
-    then "<= 4.01.1"
-    else ">= 4.01.2"
-    (* BCP: These strings seem wrong -- they should say 4.02,
-       not 4.01, according to my understanding of when the breaking
-       change happened.  However, I'm nervous about breaking installations
-       that are working, so I'm going to leave it.  Hopefully we are
-       far enough beyond these OCaml versions that it doesn't matter 
-       anyway. *)
-  in "Unison " ^ Uutil.myMajorVersion ^ " with OCaml " ^ compiler ^ "\n"
-
-let rec checkHeader conn buffer pos len =
+let receiveUntilSep ?(space=false) ?(nl=true) ?(includesep=false) conn =
+  assert (space || nl);
+  let inp = Buffer.create 32
+  and buf = Bytearray.create 1 in
+  let add () = Buffer.add_char inp buf.{0} in
+  let rec aux () =
+    grab conn.inputBuffer buf 1 >>= fun () ->
+    match buf.{0} with
+    | ' ' | '\t' when space ->
+        if includesep then add (); Lwt.return (Buffer.contents inp)
+    | '\n' when nl ->
+        if includesep then add (); Lwt.return (Buffer.contents inp)
+    | '\r' ->
+        aux () (* ignore *)
+    | _ ->
+        add (); aux ()
+  in aux ()
+
+(* Get input until newline (excluded), blocking *)
+let receiveUntilNewline conn =
+  receiveUntilSep ~nl:true conn
+
+(* Get input until space or newline, separator included by default; blocking *)
+let receiveUntilSpaceOrNl ?(includesep=true) conn =
+  receiveUntilSep ~space:true ~nl:true ~includesep conn
+
+(* Get input of fixed length, blocking *)
+let receiveString conn len =
+  let buf = Bytearray.create len in
+  grab conn.inputBuffer buf len >>= fun () ->
+  Lwt.return (Bytearray.to_string buf)
+
+(* Get input until newline (excluded), non-blocking *)
+let receiveUntilNewlineNb conn =
+  let e = Bytes.to_string (peekWithoutBlocking conn.inputBuffer) in
+  let len = try String.index e '\n' with Not_found -> String.length e in
+  receiveString conn len
+
+let sendStrings conn slist =
+  dump conn
+    (Safelist.map (fun s -> (Bytearray.of_string s, 0, String.length s)) slist)
+
+let sendString conn s = sendStrings conn [s]
+
+(****)
+
+let rpcOk = "OK\n"
+
+let rpcNokTag = "NOK "
+let rpcErr err = rpcNokTag ^ err ^ "\n"
+
+type handshakeMsg = Ok | Error of string | Unknown of string
+
+let receiveHandshakeMsg conn =
+  receiveUntilSpaceOrNl conn >>= fun msg ->
+  if msg = rpcOk then Lwt.return Ok
+  else if msg = rpcNokTag then begin
+    receiveUntilNewlineNb conn >>= fun msg -> Lwt.return (Error msg)
+  end else
+    Lwt.return (Unknown msg)
+
+type handshakeData = Data of string | Error of string | Unknown of string
+
+let receiveHandshakeData conn keyw =
+  receiveUntilSpaceOrNl conn >>= fun msg ->
+  if msg = keyw then
+    receiveUntilNewline conn >>= fun data -> Lwt.return (Data data)
+  else if msg = rpcNokTag then
+    receiveUntilNewlineNb conn >>= fun err -> Lwt.return (Error err)
+  else
+    Lwt.return (Unknown msg)
+
+let sendHandshakeMsg conn = function
+  | Ok -> sendString conn rpcOk
+  | Error err -> sendString conn (rpcErr err)
+  | Unknown _ -> assert false
+
+let sendHandshakeErr conn err =
+  sendHandshakeMsg conn (Error err)
+
+let sendHandshakeData conn keyw data =
+  let len = String.length keyw in
+  let keyw = if len > 0 && keyw.[len - 1] <> ' ' then keyw ^ " " else keyw in
+  sendString conn (keyw ^ data ^ "\n")
+
+(* RPC version negotiation process:
+   1. Server sends connectionHeader and supported RPC versions.
+
+   2. Client receives and verifies connectionHeader.
+      * If OK then proceeds.
+      * If NOK then closes connection.
+
+   3. Client receives and verifies RPC versions.
+      * If not correct version tag or can't parse then closes connection.
+
+   4. Client selects a version (typically the most recent one) from the
+      intersection of its supported RPC versions and server's RPC versions.
+      * If intersection is empty then closes connection.
+
+   5. Client sends selected RPC version to the server.
+
+   6. Server receives and verifies proposed version.
+      * If OK then proceeds.
+      * If not correct version tag, can't parse or proposed version is
+        not supported then server sends "NOK".
+      ** Client receives "NOK" and closes connection.
+
+   7. Server selects proposed version and sends "OK".
+
+   8. Client receives "OK". Version negotiation is complete.
+*)
+
+let connectionHeader = "Unison RPC\n"
+let compatConnectionHeader = "Unison 2.51 with OCaml >= 4.01.2\n"
+(* Every supported version released prior to the RPC version negotiation
+   mechanism uses this connection header string. *)
+let compat248ConnectionHeader = "Unison 2.48\n"
+(* Additionally, even 2.48 can be supported, even though that support is
+   not official. *)
+
+let rpcVersionsTag = "VERSIONS "
+let rpcVersionsStr = rpcVersionsTag ^ rpcSupportedVersionStrHdr ^ "\n"
+
+let rpcVersionTag = "VERSION "
+let rpcVersionStr ver = rpcVersionTag ^ string_of_int ver ^ "\n"
+
+let verIsSupported ver =
+  Safelist.exists (fun v -> v = ver) rpcSupportedVersions
+
+let handshakeFail err =
+  Lwt.fail (Util.Fatal err)
+
+let handshakeError msg =
+  handshakeFail ("Received error from the server: \"" ^ msg ^ "\".")
+
+let handshakeUnknown msg =
+  handshakeFail ("Received unexpected header from the server: \""
+                 ^ String.escaped msg ^ "\".")
+
+let parseVersion side s =
+  let error e =
+    raise (Util.Transient
+            ("Unknown " ^ side ^ " RPC version: " ^ e
+             ^ ". Version received from " ^ side ^ ": \"" ^ String.escaped s
+             ^ "\". Supported RPC versions: " ^ rpcSupportedVersionStr))
+  in
+  if s = "" then
+    error "invalid format"
+  else
+    match int_of_string s with
+    | ver -> Some ver
+    | exception Failure _ -> error "parse error"
+
+let parseServerVersions inp =
+  let supported l = function
+    | "" -> l
+    | v -> match parseVersion "server" v with
+           | Some vi -> if verIsSupported vi then vi :: l else l
+           | None -> l
+  in
+  try
+    let vs = String.split_on_char ' ' inp in
+    if vs = [""] then ignore (parseVersion "server" ""); (* Trigger the error *)
+    let intersect = Safelist.fold_left supported [] vs in
+    Lwt.return (Safelist.rev (Safelist.sort compare intersect))
+  with
+  | Util.Transient e -> handshakeFail e
+
+let selectServerVersion conn =
+  let getTheRest () = Bytes.to_string (peekWithoutBlocking conn.inputBuffer) in
+  receiveHandshakeData conn rpcVersionsTag >>= function
+  | Error msg -> handshakeError msg
+  | Unknown fromServ -> handshakeUnknown (fromServ ^ getTheRest ())
+  | Data versions ->
+      parseServerVersions versions >>= function
+      | [] ->
+          handshakeFail ("None of server's RPC versions are supported. "
+                         ^ "The server may be too old or too recent. "
+                         ^ "Versions received from server: \""
+                         ^ String.escaped versions ^ "\". "
+                         ^ "Supported RPC versions: " ^ rpcSupportedVersionStr)
+      | ver :: _ ->
+          setConnectionVersion conn ver;
+          debug (fun () -> Util.msg "Selected RPC version: %i\n" ver);
+          sendHandshakeData conn rpcVersionTag (string_of_int ver) >>= fun () ->
+          receiveHandshakeMsg conn >>= function
+          | Ok -> Lwt.return ()
+          | Error reply -> handshakeError reply
+          | Unknown reply -> handshakeUnknown (reply ^ getTheRest ())
+
+let checkServerVersion conn header =
+  if header = compatConnectionHeader then begin
+    setConnectionVersion conn 0;
+    debug (fun () -> Util.msg "Selected RPC version: 2.51-compatibility\n");
+    (* skip negotiation *) Lwt.return ()
+  end else if header = compat248ConnectionHeader then begin
+    setConnectionVersion conn 0;
+    debug (fun () -> Util.msg "Selected RPC version: 2.48-compatibility\n");
+    (* skip negotiation *) Lwt.return ()
+  end else
+    selectServerVersion conn
+
+let rec checkHeaderRec conn buffer pos len connectionHeader =
   if pos = len then
-    Lwt.return ()
+    Lwt.return connectionHeader
   else begin
     (grab conn.inputBuffer buffer 1 >>= (fun () ->
-    if buffer.{0} <> connectionHeader.[pos] then
+    let chOk =
+      try buffer.{0} = connectionHeader.[pos] with Invalid_argument _ -> false
+    and compatChOk =
+      try buffer.{0} = compatConnectionHeader.[pos] with Invalid_argument _ -> false
+    and compat248ChOk =
+      try buffer.{0} = compat248ConnectionHeader.[pos] with Invalid_argument _ -> false
+    in
+    if not chOk && not compatChOk && not compat248ChOk then
       let prefix =
         String.sub connectionHeader 0 pos ^ Bytearray.to_string buffer in
       let rest = peekWithoutBlocking conn.inputBuffer in
@@ -954,9 +1473,97 @@ let rec checkHeader conn buffer pos len =
            ^ "message, or because your remote login shell is printing\n"
            ^ "something itself before starting Unison."))
     else
-      checkHeader conn buffer (pos + 1) len))
+    if not chOk && compatChOk then
+      (* We make use of the fact that that the new header is almost a prefix
+         of the old header. It is not an exact comparison here but good
+         enough for this purpose. *)
+      checkHeaderRec conn buffer (pos + 1)
+        (String.length compatConnectionHeader) compatConnectionHeader
+    else if not chOk && compat248ChOk then
+      checkHeaderRec conn buffer (pos + 1)
+        (String.length compat248ConnectionHeader) compat248ConnectionHeader
+    else
+      checkHeaderRec conn buffer (pos + 1) len connectionHeader))
   end
 
+let checkHeader conn =
+  checkHeaderRec conn (Bytearray.create 1) 0
+    (String.length connectionHeader) connectionHeader
+
+(****)
+
+(* Magic string exchange is used within the old protocol to detect if both
+   the server and the client support the new RPC version negotiation mechanism.
+
+   It works like this:
+    1. Directly after connection header, the server sends the magic string and
+       otherwise continues using the old RPC protocol.
+    2. An old client will process the magic string as a valid RPC message that
+       is effectively a no-op and continues using old RPC protocol as normal.
+    3. A new client will notice the magic string and send the same magic string
+       in response. It will stop the old RPC protocol and restart from header
+       checking and what is now hopefully an RPC version negotiation.
+    4. The server will notice client's magic string, stop the old RPC protocol
+       and restart from connection header, this time with the new RPC version
+       negotiation mechanism.
+
+   The magic string is defined as follows:
+    1. encoded int 1 followed by
+    2. encoded int > 0 (packet size) followed by
+    3. a valid 2.51 protocol packet, the contents of which we don't care about,
+       but it must be a no-op for 2.51 client (in this case a StreamAbort).
+
+   Int 1 is a valid 2.51 protocol message ID but it is never used with normal
+   messages, hence its safe usage as a magic string. A StreamAbort to a client,
+   especially with id 1, is a safe no-op. *)
+
+let magicId = 1
+(* Although this magic packet is inherently dependent on OCaml version,
+   it is unlikely to change and has been verified to be the same with
+   OCaml versions 4.05 to 4.12. It is hard coded here to avoid any future
+   changes (the idea being that old clients will not be compiled with
+   any newer OCaml compilers). *)
+let magicPacket = "rsp\132\149\166\190\000\000\000\001\000\000\000\000\000\000\000\000\000\000\000\000A"
+let magic = encodeInt magicId :: encodeInt (String.length magicPacket) ::
+              [Bytearray.of_string magicPacket, 0, String.length magicPacket]
+
+let checkForMagicString conn =
+  (* Fill the buffer and then peek at the contents without consuming *)
+  peekWithBlocking conn.inputBuffer >>= fun b ->
+  if Bytes.length b < intSize then
+    Lwt.return false
+  else begin
+    let id = Bytearray.create intSize in
+    let () = Bytearray.blit_from_bytes b 0 id 0 intSize in
+    if decodeInt id 0 <> magicId then
+      Lwt.return false
+    else begin
+      debug (fun () -> Util.msg "Received RPC version upgrade notice\n");
+      (* Consume magic id from buffer *)
+      grab conn.inputBuffer id intSize >>= fun () ->
+      (* Consume magic packet from buffer *)
+      receivePacket conn >>= fun _ -> Lwt.return true
+      (* We rely solely on the magic id and don't check the contents of the
+         packet. Should it become necessary for some reason then it is
+         possible to verify the magic packet byte by byte here. *)
+    end
+  end
+
+let checkServerUpgrade conn header =
+  if header <> compatConnectionHeader && header <> compat248ConnectionHeader then
+    Lwt.return header
+  else
+    checkForMagicString conn >>= function
+    | false -> Lwt.return header
+    | true ->
+        (* Consume write token from buffer *)
+        let id = Bytearray.create intSize in
+        grab conn.inputBuffer id intSize >>= fun () ->
+        (* Send the magic string *)
+        dumpUrgent conn magic >>= fun () ->
+        debug (fun () -> Util.msg "Going to attempt RPC version upgrade\n");
+        checkHeader conn
+
 (****)
 
 (*
@@ -966,24 +1573,23 @@ let rec checkHeader conn buffer pos len =
 *)
 let halfduplex =
   Prefs.createBool "halfduplex" false
-    "!force half-duplex communication with the server"
+    ~category:(`Advanced `Remote)
+    ~deprecated:true
+    "force half-duplex communication with the server"
     "When this flag is set to {\\tt true}, Unison network communication \
      is forced to be half duplex (the client and the server never \
      simultaneously emit data).  If you experience unstabilities with \
-     your network link, this may help.  The communication is always \
-     half-duplex when synchronizing with a Windows machine due to a \
-     limitation of Unison current implementation that could result \
-     in a deadlock."
+     your network link, this may help."
 
 let negociateFlowControlLocal conn () =
   disableFlowControl conn.outputQueue;
   Lwt.return false
 
 let negociateFlowControlRemote =
-  registerServerCmd "negociateFlowControl" negociateFlowControlLocal
+  registerServerCmd "negociateFlowControl" Umarshal.unit Umarshal.bool negociateFlowControlLocal
 
 let negociateFlowControl conn =
-  (* Flow control negociation can be done asynchronously. *)
+  (* Flow control negotiation can be done asynchronously. *)
   if not (Prefs.read halfduplex) then
     Lwt.ignore_result
       (negociateFlowControlRemote conn () >>= fun needed ->
@@ -994,16 +1600,34 @@ let negociateFlowControl conn =
 
 (****)
 
-let initConnection onClose in_ch out_ch =
-  let conn = setupIO false in_ch out_ch in
-  checkHeader
-    conn (Bytearray.create 1) 0 (String.length connectionHeader) >>= (fun () ->
+let initConnection ?(connReady=fun () -> ()) ?cleanup in_ch out_ch =
+  (* [makeConnection] is not expected to raise any recoverable exceptions.
+     If this assumption changes in the future then [in_ch] and [out_ch] must
+     be closed in the recovery code. *)
+  let conn = makeConnection false in_ch out_ch in
+  let close_on_fail t =
+    Lwt.catch (fun () -> t) (fun e -> closeConnection conn; Lwt.fail e)
+  in
+  let with_timeout t =
+    Lwt.choose [t;
+      Lwt_unix.sleep 120. >>= fun () ->
+      Lwt.fail (Util.Fatal "Timed out negotiating connection with the server")]
+  in
+  close_on_fail (with_timeout (
+    peekWithBlocking conn.inputBuffer >>= fun _ ->
+    connReady (); Lwt.return () >>= fun () -> (* Connection working, notify *)
+    checkHeader conn >>=
+    checkServerUpgrade conn >>=
+    checkServerVersion conn)) >>= fun () ->
+  registerConnCleanup conn cleanup;
+  (* From this moment forward, the RPC version has been selected. All
+     communication must now adhere to that version's specification. *)
+  enableFlowControl conn false >>= (fun () ->
   Lwt.ignore_result (Lwt.catch
     (fun () -> receive conn)
-    (function
-     | Util.Fatal "Lost connection with the server" as e -> onClose e
-     | e -> Lwt.fail e
-    ));
+    (fun e ->
+      clientConnClose conn;
+      if isConnectionCheck conn then Lwt.return () else Lwt.fail e));
   negociateFlowControl conn;
   Lwt.return conn)
 
@@ -1018,16 +1642,16 @@ let rec findFirst f l =
 let printAddr host addr =
   match addr with
     Unix.ADDR_UNIX s ->
-      assert false
+      s
   | Unix.ADDR_INET (s, p) ->
       Format.sprintf "%s[%s]:%d" host (Unix.string_of_inet_addr s) p
 
-let buildSocket host port kind ai =
+let buildSocket host port kind ?(err="") ai =
   let attemptCreation ai =
     Lwt.catch
       (fun () ->
          let socket =
-           Lwt_unix.socket
+           Lwt_unix.socket ~cloexec:true
              ai.Unix.ai_family ai.Unix.ai_socktype ai.Unix.ai_protocol
          in
          Lwt.catch
@@ -1043,8 +1667,10 @@ let buildSocket host port kind ai =
                   if ai.Unix.ai_family = Unix.PF_INET6 then
                     Lwt_unix.setsockopt socket Unix.IPV6_ONLY true;
                   (* Allow reuse of local addresses for bind *)
-                  Lwt_unix.setsockopt socket Unix.SO_REUSEADDR true;
-                  (* Bind the socket to portnum on the local host *)
+                  if ai.Unix.ai_family <> Unix.PF_UNIX then
+                    Lwt_unix.setsockopt socket Unix.SO_REUSEADDR true;
+                  (* Bind the socket to portnum on the local host
+                     or to a filesystem path (when Unix domain socket) *)
                   Lwt_unix.bind socket ai.Unix.ai_addr;
                   (* Start listening, allow up to 1 pending request *)
                   Lwt_unix.listen socket 1;
@@ -1063,15 +1689,16 @@ let buildSocket host port kind ai =
            Unix.Unix_error (error, _, _) ->
              begin match error with
                Unix.EAFNOSUPPORT | Unix.EPROTONOSUPPORT | Unix.EINVAL ->
-                 ()
+                 Lwt.return None
              | _  ->
                  let msg =
                    match kind with
                      `Connect ->
-                       Printf.sprintf "Can't connect to server %s: %s\n"
+                       Printf.sprintf "%s%s: %s\n"
+                         err
                          (printAddr host ai.Unix.ai_addr)
                          (Unix.error_message error)
-                   | `Bind ->
+                   | `Bind when ai.Unix.ai_family <> Unix.PF_UNIX ->
                        Printf.sprintf
                          "Can't bind socket to port %s at address [%s]: %s\n"
                          port
@@ -1081,56 +1708,99 @@ let buildSocket host port kind ai =
                           | _ ->
                               assert false)
                          (Unix.error_message error)
+                   | `Bind (* Unix.PF_UNIX *) ->
+                       Printf.sprintf
+                         "Can't bind socket to path '%s': %s\n"
+                         port
+                         (Unix.error_message error)
                  in
-                 Util.warn msg
-              end;
-              Lwt.return None
+                 Lwt.fail (Util.Fatal msg)
+             end
          | _ ->
              Lwt.fail e)
   in
   attemptCreation ai
 
+let makeUnixSocketAi path =
+  { Unix.ai_family = Unix.PF_UNIX;
+    ai_socktype = Unix.SOCK_STREAM;
+    ai_protocol = 0;
+    ai_addr = Unix.ADDR_UNIX path;
+    ai_canonname = "" }
+
+let buildConnectSocketUnix path =
+  assert (String.length path > 2);
+  (* Unix domain socket path from [Clroot] is enclosed in curly braces.
+     Extract the real path. *)
+  let path = String.sub path 1 ((String.length path) - 2) in
+  let err = "Can't connect to Unix domain socket on path " in
+  buildSocket "" path `Connect ~err (makeUnixSocketAi path) >>= function
+  | None ->
+      Lwt.fail (Util.Fatal (err ^ path))
+  | Some x ->
+      Lwt.return x
+
 let buildConnectSocket host port =
-  let attemptCreation ai = buildSocket host port `Connect ai in
+  let isHost = String.length host > 0 && host.[0] <> '{' in
+  if not isHost then buildConnectSocketUnix host else
+  let err = "Failed to connect to the server on host " in
+  let attemptCreation ai = buildSocket host port `Connect ~err ai in
   let options = [ Unix.AI_SOCKTYPE Unix.SOCK_STREAM ] in
   findFirst attemptCreation (Unix.getaddrinfo host port options) >>= fun res ->
   match res with
     Some socket ->
       Lwt.return socket
   | None ->
-      let msg =
-        Printf.sprintf
-          "Failed to connect to the server on host %s:%s" host port
-      in
-      Lwt.fail (Util.Fatal msg)
+      let hostport = Printf.sprintf "%s:%s" host port in
+      Lwt.fail (Util.Fatal (err ^ hostport))
+
+(* [at_exit] does not provide reliable cleanup (why?), so this
+   complex mechanism is needed to unlink Unix domain sockets
+   in case of exceptional termination. *)
+let createdUnixSockets = ref []
+
+let postponeUnixSocketCleanup path =
+  createdUnixSockets := path :: !createdUnixSockets
 
-let buildListenSocket host port =
+let unixSocketCleanup () =
+  Safelist.iter
+    (fun path -> try Unix.unlink path with Unix.Unix_error _ -> ())
+    !createdUnixSockets
+
+let buildListenSocketUnix path =
+  assert (path <> "");
+  buildSocket "" path `Bind (makeUnixSocketAi path) >>= function
+  | None ->
+      Lwt.fail (Util.Fatal
+        (Printf.sprintf "Can't bind Unix domain socket on path %s" path))
+  | Some x ->
+      postponeUnixSocketCleanup path;
+      Lwt.return [x]
+
+let buildListenSocket hosts port =
+  let isPort = try ignore (int_of_string port); true with Failure _ -> false in
+  if not isPort then buildListenSocketUnix port else
   let options = [ Unix.AI_SOCKTYPE Unix.SOCK_STREAM ; Unix.AI_PASSIVE ] in
-  Lwt_util.map (buildSocket host port `Bind)
-    (Unix.getaddrinfo host port options) >>= fun res ->
+  hosts
+  |> Safelist.map (fun host -> Unix.getaddrinfo host port options)
+  |> Safelist.concat
+  |> Lwt_util.map (buildSocket "" port `Bind) >>= fun res ->
   match Safelist.filter (fun x -> x <> None) res with
   | [] ->
-      let msg =
-        if host = "" then
-          Printf.sprintf "Can't bind socket to port %s" port
-        else
-          Printf.sprintf "Can't bind socket to port %s on host %s"
-            port host
-      in
-      Lwt.fail (Util.Fatal msg)
+      Lwt.fail (Util.Fatal (Printf.sprintf "Can't bind socket to port %s" port))
   | s ->
       Lwt.return (Safelist.map (function None -> assert false | Some x -> x) s)
 
-let buildSocketConnection onClose host port =
+let buildSocketConnection host port =
   buildConnectSocket host port >>= fun socket ->
-  initConnection (onClose (fun () -> ())) socket socket
+  initConnection socket socket
 
-let buildShellConnection onClose shell host userOpt portOpt rootName termInteract =
+let buildShellConnection shell host userOpt portOpt rootName termInteract =
   let remoteCmd =
     (if Prefs.read serverCmd="" then Uutil.myName
      else Prefs.read serverCmd)
     ^ (if Prefs.read addversionno then "-" ^ Uutil.myMajorVersion else "")
-    ^ " -server" in
+    ^ " -server " ^ rpcServerCmdlineOverride in
   let userArgs =
     match userOpt with
       None -> []
@@ -1142,15 +1812,11 @@ let buildShellConnection onClose shell host userOpt portOpt rootName termInterac
   let shellCmd =
     (if shell = "ssh" then
       Prefs.read sshCmd
-    else if shell = "rsh" then
-      Prefs.read rshCmd
     else
       shell) in
   let shellCmdArgs =
     (if shell = "ssh" then
       Prefs.read sshargs
-    else if shell = "rsh" then
-      Prefs.read rshargs
     else
       "") in
   let preargs =
@@ -1164,13 +1830,11 @@ let buildShellConnection onClose shell host userOpt portOpt rootName termInterac
     Safelist.concat
       (Safelist.map (fun s -> Util.splitIntoWords s ' ') preargs) in
   let argsarray = Array.of_list args in
-  let (i1,o1) = Lwt_unix.pipe_out () in
-  let (i2,o2) = Lwt_unix.pipe_in () in
+  let (i1,o1) = Lwt_unix.pipe_out ~cloexec:true () in
+  let (i2,o2) = Lwt_unix.pipe_in ~cloexec:true () in
   (* We need to make sure that there is only one reader and one
      writer by pipe, so that, when one side of the connection
      dies, the other side receives an EOF or a SIGPIPE. *)
-  Lwt_unix.set_close_on_exec i2;
-  Lwt_unix.set_close_on_exec o1;
   (* We add CYGWIN=binmode to the environment before calling
      ssh because the cygwin implementation on Windows sometimes
      puts the pipe in text mode (which does end of line
@@ -1182,133 +1846,173 @@ let buildShellConnection onClose shell host userOpt portOpt rootName termInterac
   System.putenv "CYGWIN" "binmode";
   debug (fun ()-> Util.msg "Shell connection: %s (%s)\n"
            shellCmd (String.concat ", " args));
-  let term =
+  let (term, termPid) =
     Util.convertUnixErrorsToFatal "starting shell connection" (fun () ->
     match termInteract with
-      None ->
-        ignore (System.create_process shellCmd argsarray i1 o2 Unix.stderr);
-        None
+    | None ->
+        (* Signals generated by the terminal from user input are sent to all
+           processes in the foreground process group. This means that the ssh
+           child process will receive SIGINT at the same time as Unison and
+           close the connection before Unison has the chance to do cleanup with
+           the remote end. To make matters more complicated, the ssh process
+           must be in the foreground process group because interaction with the
+           user is done via the terminal (not via stdin, stdout) and background
+           processes can't read from the terminal (unless we'd set up a pty
+           like is done for the GUI).
+
+           Don't let these signals reach ssh by blocking them.
+
+           The signals could be ignored instead of being blocked because ssh
+           does not set handlers for SIGINT and SIGQUIT if they've been ignored
+           at startup. But this triggers an error in ssh. The interactive
+           passphrase reading function captures these signals for the purpose
+           of restoring terminal settings (echo). When receiving a signal, and
+           after restoring previous signal handlers, it resends the signal to
+           itself. But now the signal is ignored and instead of terminating,
+           the process will continue running as if passphrase reading function
+           had returned with an empty result.
+
+           Since the ssh process no longer receives the signals generated by
+           user input we have to make sure that it terminates when Unison does.
+           This usually happens due to its stdin and stdout being closed,
+           except for when it is interacting with the user via terminal. To get
+           around that, an [at_exit] handler is registered to send a SIGTERM
+           and SIGKILL to the ssh process.  (Note, for [at_exit] handlers to
+           run, unison process must terminate normally, not be killed. For
+           SIGINT, this means that [Sys.catch_break true] (or an alternative
+           SIGINT handler) must be set before creating the ssh process.) *)
+        let pid = Util.blockSignals [Sys.sigint] (fun () ->
+          System.create_process shellCmd argsarray i1 o2 Unix.stderr) in
+        let end_ssh () =
+          let kill_noerr si = try Unix.kill pid si
+            with Unix.Unix_error _ -> () | Invalid_argument _ -> () in
+          match Unix.waitpid [WNOHANG] pid with
+          | (0, _) ->
+              (* Grace period before killing. Important to give ssh a chance
+                 to restore terminal settings, should that be needed. *)
+              kill_noerr Sys.sigterm; Unix.sleepf 0.01; kill_noerr Sys.sigkill
+          | _ | exception Unix.Unix_error _ -> ()
+        in
+        let () = at_exit end_ssh in
+        (None, pid)
     | Some callBack ->
-        fst (Terminal.create_session shellCmd argsarray i1 o2 Unix.stderr))
+        Terminal.create_session shellCmd argsarray i1 o2 Unix.stderr)
   in
   Unix.close i1; Unix.close o2;
-  begin match term, termInteract with
-  | Some fdTerm, Some callBack ->
-      Terminal.handlePasswordRequests fdTerm (callBack rootName)
-  | _ ->
-      ()
-  end;
+  let forwardShellStderr fdIn fdOut s =
+    (* When the shell connection has been established then keep
+       forwarding server's stderr to client's stderr; not to GUI. *)
+    let buf = Bytes.create 16000 in
+    let rec loop s len =
+      (* Can't use printf because if stderr is not open in Windows,
+         it will throw an exception when at_exit tries to flush it. *)
+      ignore (try if len > 0 then Unix.write fdOut s 0 len else 0
+              with Unix.Unix_error _ -> 0);
+      Lwt.catch (fun () -> Lwt_unix.read fdIn buf 0 16000)
+        (fun _ -> debug (fun () ->
+           Util.msg "Caught an exception when reading remote stderr\n");
+           Lwt.return 0)
+      >>= function
+      | 0 -> Lwt.return ()
+      | len -> loop buf len
+    in
+    loop (Bytes.of_string s) (String.length s)
+  in
+  let est = ref false in
+  let connReady () = est := true
+  and isReady () = !est = true in
+  let getTermErr =
+    match term, termInteract with
+    | Some fdTerm, Some interact ->
+        let (handleRequests, extractRemainingOutput) =
+          Terminal.handlePasswordRequests fdTerm (interact rootName) isReady in
+        Lwt.ignore_result (
+          handleRequests >>= fun () ->
+          extractRemainingOutput false >>=
+          forwardShellStderr (fst fdTerm) Unix.stderr);
+        fun () -> extractRemainingOutput true
+    | _ ->
+        fun () -> Lwt.return ""
+  in
   let cleanup () =
-    try Terminal.close_session term with Unix.Unix_error _ -> ()
+    (* Make sure the [handlePasswordRequests] threads will finish while
+       silencing any exceptions (most likely EBADF) caused by having closed
+       the terminal fds. *)
+    Lwt.ignore_result (getTermErr () >>= fun s ->
+      debug (fun () ->
+        if s <> "" then Util.msg "Received from remote shell process:\n%s\n" s);
+      Lwt.return ());
+    if term = None then
+      try ignore (Terminal.safe_waitpid termPid) with Unix.Unix_error _ -> ()
+    else
+      try Terminal.close_session termPid with Unix.Unix_error _ -> ()
   in
-  initConnection (onClose cleanup) i2 o1
-
-let canonizeLocally s unicode =
-  (* We need to select the proper API in order to compute correctly the
-     canonical fspath *)
-  Fs.setUnicodeEncoding unicode;
+  (* With [connReady], we know that shell connection was established (even if
+     RPC handshake failed). This hacky way of detecting the connection is used
+     because [Lwt_unix.wait_read] is not implemented under Windows.
+     By this time, we are already somewhat late in the communication process.
+     Any error output from very early stages of server startup, before other
+     output is produced, might still end up in GUI (but this is very unlikely;
+     it is more likely that the same error caused connection to be dropped). *)
+  Lwt.catch
+    (fun () -> initConnection ~connReady ~cleanup i2 o1)
+    (fun e ->
+      Lwt.catch
+        (fun () -> getTermErr () >>= fun s ->
+                   if s <> "" then Util.warn s;
+                   Lwt.fail e)
+        (* Don't close the terminal before reading the final error output
+           or we might miss it completely. *)
+        (fun _ ->  cleanup (); Lwt.fail e))
+
+let canonizeLocally s =
   Fspath.canonize s
 
 let canonizeOnServer =
   registerServerCmd "canonizeOnServer"
-    (fun _ (s, unicode) ->
-       Lwt.return (Os.myCanonicalHostName (), canonizeLocally s unicode))
+    Umarshal.(prod2 (option string) bool id id)
+    Umarshal.(prod2 string Fspath.m id id)
+    (fun _ (s, _) -> (* The tuple is kept for backwards API compatibility *)
+       Lwt.return (Os.myCanonicalHostName (), canonizeLocally s))
+
+let canonizeOnServer conn s =
+  (* The second tuple item is required for compatibility with <= 2.52 *)
+  canonizeOnServer conn (s, true)
 
 let canonize clroot = (* connection for clroot must have been set up already *)
   match clroot with
     Clroot.ConnectLocal s ->
-      (Common.Local, canonizeLocally s (Case.useUnicodeAPI ()))
+      (Common.Local, canonizeLocally s)
   | _ ->
-      match
-        try
-          Some (Safelist.assoc clroot !connectedHosts)
-        with Not_found ->
-          None
-      with
+      match ClientConn.canonRootOfClroot clroot with
         None                -> raise (Util.Fatal "Remote.canonize")
-      | Some (h, fspath, _) -> (Common.Remote h, fspath)
-
-let listReplace v l = v :: Safelist.remove_assoc (fst v) l
-
-let connectionCheck = ref ""
-
-let checkConnection host ioServer =
-  connectionCheck := host;
-  (* Poke on the socket to trigger an error if connection has been lost. *)
-  Lwt_unix.run (
-    (if (Util.osType = `Win32) then Lwt.return 0 else
-    Lwt_unix.read ioServer.inputBuffer.channel ioServer.inputBuffer.buffer 0 0)
-    (* Try to make sure connection cleanup, if necessary, has finished
-       before returning.
-       Since there is no way to reliably detect when other threads have
-       finished, we just yield a bit (the same comments apply as in
-       commandLoop). *)
-    >>= fun _ ->
-    let rec wait n =
-      if n = 0 then Lwt.return () else begin
-        Lwt_unix.yield () >>= fun () ->
-        wait (n - 1)
-      end
-    in
-    wait 10);
-  connectionCheck := ""
-
-let rec hostFspath clroot =
-  try
-    let (host, _, ioServer) = Safelist.assoc clroot !connectedHosts in
-    checkConnection host ioServer;
-    let (_, _, ioServer) = Safelist.assoc clroot !connectedHosts in
-    Some (Lwt.return ioServer)
-  with Not_found ->
-    None
+      | Some root -> root
 
 let isRootConnected = function
   | (Common.Local, _) -> true
-  | (Common.Remote host, _) -> begin
-      try
-        let ioServer = Safelist.assoc host !connectionsByHosts in
-        checkConnection host ioServer;
-        let _ = Safelist.assoc host !connectionsByHosts in
-        true
-      with Not_found ->
-        false
-    end
-
-let onClose clroot cleanup e =
-  try
-    let (host, _, _) = Safelist.assoc clroot !connectedHosts in
-    connectedHosts := Safelist.remove_assoc clroot !connectedHosts;
-    connectionsByHosts := Safelist.remove_assoc host !connectionsByHosts;
-    cleanup ();
-    if !connectionCheck = host then Lwt.return ()
-    else Lwt.fail e
-  with Not_found ->
-    if !connectionCheck = "" then Lwt.fail e
-    else Lwt.return ()
+  | (Common.Remote _, _) as root -> ClientConn.ofRootConncheck root <> None
 
 let canonizeRoot rootName clroot termInteract =
-  let unicode = Case.useUnicodeAPI () in
   let finish ioServer s =
-    (* We need to always compute the fspath as it depends on
-       unicode settings *)
-    canonizeOnServer ioServer (s, unicode) >>= (fun (host, fspath) ->
-    connectedHosts :=
-      listReplace (clroot, (host, fspath, ioServer)) !connectedHosts;
-    connectionsByHosts := listReplace (host, ioServer) !connectionsByHosts;
-    Lwt.return (Common.Remote host,fspath)) in
+    (* We need to always compute the fspath as it may have changed
+       due to profile configuration changes *)
+    canonizeOnServer ioServer s >>= (fun (host, fspath) ->
+    let root = (Common.Remote host, fspath) in
+    ClientConn.register clroot root ioServer;
+    Lwt.return root) in
   match clroot with
     Clroot.ConnectLocal s ->
-      Lwt.return (Common.Local, canonizeLocally s unicode)
+      Lwt.return (Common.Local, canonizeLocally s)
   | Clroot.ConnectBySocket(host,port,s) ->
-      begin match hostFspath clroot with
-        Some x -> x
-      | None   -> buildSocketConnection (onClose clroot) host port
+      begin match ClientConn.ofClrootConncheck clroot with
+      | Some x -> Lwt.return x
+      | None   -> buildSocketConnection host port
       end >>= fun ioServer ->
       finish ioServer s
   | Clroot.ConnectByShell(shell,host,userOpt,portOpt,s) ->
-      begin match hostFspath clroot with
-        Some x -> x
-      | None   -> buildShellConnection (onClose clroot)
+      begin match ClientConn.ofClrootConncheck clroot with
+      | Some x -> Lwt.return x
+      | None   -> buildShellConnection
                    shell host userOpt portOpt rootName termInteract
       end >>= fun ioServer ->
       finish ioServer s
@@ -1332,33 +2036,24 @@ let openConnectionStart clroot =
       None
   | Clroot.ConnectBySocket(host,port,s) ->
       Lwt_unix.run
-        (begin match hostFspath clroot with
-           Some x -> x
-         | None   -> buildSocketConnection (onClose clroot) host port
+        (begin match ClientConn.ofClrootConncheck clroot with
+         | Some x -> Lwt.return x
+         | None   -> buildSocketConnection host port
          end >>= fun ioServer ->
-         (* We need to always compute the fspath as it depends on
-            unicode settings *)
-         let unicode = Case.useUnicodeAPI () in
-         canonizeOnServer ioServer (s, unicode) >>= fun (host, fspath) ->
-         connectedHosts :=
-           listReplace (clroot, (host, fspath, ioServer)) !connectedHosts;
-         connectionsByHosts :=
-           listReplace (host, ioServer) !connectionsByHosts;
+         (* We need to always compute the fspath as it may have changed
+            due to profile configuration changes *)
+         canonizeOnServer ioServer s >>= fun (host, fspath) ->
+         ClientConn.register clroot (Common.Remote host, fspath) ioServer;
          Lwt.return ());
       None
   | Clroot.ConnectByShell(shell,host,userOpt,portOpt,s) ->
-      match hostFspath clroot with
-         Some x ->
-           let unicode = Case.useUnicodeAPI () in
+      match ClientConn.ofClrootConncheck clroot with
+      | Some ioServer ->
            (* We recompute the fspath as it may have changed due to
-              unicode settings *)
+              profile configuration changes *)
            Lwt_unix.run
-             (x >>= fun ioServer ->
-              canonizeOnServer ioServer (s, unicode) >>= fun (host, fspath) ->
-              connectedHosts :=
-                listReplace (clroot, (host, fspath, ioServer)) !connectedHosts;
-              connectionsByHosts :=
-                listReplace (host, ioServer) !connectionsByHosts;
+             (canonizeOnServer ioServer s >>= fun (host, fspath) ->
+              ClientConn.register clroot (Common.Remote host, fspath) ioServer;
               Lwt.return ());
            None
       | None ->
@@ -1366,7 +2061,7 @@ let openConnectionStart clroot =
             (if Prefs.read serverCmd="" then Uutil.myName
              else Prefs.read serverCmd)
             ^ (if Prefs.read addversionno then "-" ^ Uutil.myMajorVersion else "")
-            ^ " -server" in
+            ^ " -server " ^ rpcServerCmdlineOverride in
           let userArgs =
             match userOpt with
               None -> []
@@ -1378,15 +2073,11 @@ let openConnectionStart clroot =
           let shellCmd =
             (if shell = "ssh" then
               Prefs.read sshCmd
-            else if shell = "rsh" then
-              Prefs.read rshCmd
             else
               shell) in
           let shellCmdArgs =
             (if shell = "ssh" then
               Prefs.read sshargs
-            else if shell = "rsh" then
-              Prefs.read rshargs
             else
               "") in
           let preargs =
@@ -1400,13 +2091,11 @@ let openConnectionStart clroot =
             Safelist.concat
               (Safelist.map (fun s -> Util.splitIntoWords s ' ') preargs) in
           let argsarray = Array.of_list args in
-          let (i1,o1) = Lwt_unix.pipe_out() in
-          let (i2,o2) = Lwt_unix.pipe_in() in
+          let (i1,o1) = Lwt_unix.pipe_out ~cloexec:true () in
+          let (i2,o2) = Lwt_unix.pipe_in ~cloexec:true () in
           (* We need to make sure that there is only one reader and one
              writer by pipe, so that, when one side of the connection
              dies, the other side receives an EOF or a SIGPIPE. *)
-          Lwt_unix.set_close_on_exec i2;
-          Lwt_unix.set_close_on_exec o1;
           (* We add CYGWIN=binmode to the environment before calling
              ssh because the cygwin implementation on Windows sometimes
              puts the pipe in text mode (which does end of line
@@ -1440,55 +2129,128 @@ let openConnectionReply = function
 let openConnectionEnd (i1,i2,o1,o2,s,fdopt,clroot,pid) =
       Unix.close i1; Unix.close o2;
       let cleanup () =
-        try Terminal.close_session fdopt with Unix.Unix_error _ -> ()
+        try Terminal.close_session pid with Unix.Unix_error _ -> ()
       in
       Lwt_unix.run
-        (initConnection (onClose clroot cleanup) i2 o1 >>= fun ioServer ->
-         let unicode = Case.useUnicodeAPI () in
-         canonizeOnServer ioServer (s, unicode) >>= fun (host, fspath) ->
-         connectedHosts :=
-           listReplace (clroot, (host, fspath, ioServer)) !connectedHosts;
-         connectionsByHosts :=
-           listReplace (host, ioServer) !connectionsByHosts;
+        (initConnection ~cleanup i2 o1 >>= fun ioServer ->
+         canonizeOnServer ioServer s >>= fun (host, fspath) ->
+         ClientConn.register clroot (Common.Remote host, fspath) ioServer;
          Lwt.return ())
 
 let openConnectionCancel (i1,i2,o1,o2,s,fdopt,clroot,pid) =
-      try Unix.kill pid Sys.sigkill with Unix.Unix_error _ -> ();
-      try Unix.close i1 with Unix.Unix_error _ -> ();
-      try Lwt_unix.close i2 with Unix.Unix_error _ -> ();
-      try Lwt_unix.close o1 with Unix.Unix_error _ -> ();
-      try Unix.close o2 with Unix.Unix_error _ -> ();
-      match fdopt with
-        None   -> ()
-      | Some _ -> (try Terminal.close_session fdopt with Unix.Unix_error _ -> ())
+  (try Unix.kill pid Sys.sigkill with Unix.Unix_error _ -> ());
+  (try Unix.close i1 with Unix.Unix_error _ -> ());
+  (try Lwt_unix.close i2 with Unix.Unix_error _ -> ());
+  (try Lwt_unix.close o1 with Unix.Unix_error _ -> ());
+  (try Unix.close o2 with Unix.Unix_error _ -> ());
+  (try Terminal.close_session pid with Unix.Unix_error _ -> ())
 
 (****************************************************************************)
 (*                     SERVER-MODE COMMAND PROCESSING LOOP                  *)
 (****************************************************************************)
 
+let checkClientVersion conn () =
+  let reply msg = sendHandshakeMsg conn msg in
+  (* FIX: In future when gaining the ability to close connections from server
+     side, make errors close the connection, not just send to client. *)
+  let error = sendHandshakeErr conn in
+  receiveHandshakeData conn rpcVersionTag >>= function
+  | Error msg ->
+      error ("Could not negotiate RPC version. "
+             ^ "Received unexpected error from the client: \"" ^ msg ^ "\"")
+  | Unknown fromClient ->
+      error ("Could not negotiate RPC version. "
+             ^ "Received unexpected header from the client: \""
+             ^ String.escaped (fromClient
+             ^ Bytes.to_string (peekWithoutBlocking conn.inputBuffer)) ^ "\"")
+  | Data buf ->
+      match parseVersion "client" buf with
+      | Some clientVer ->
+          if verIsSupported clientVer then begin
+            setConnectionVersion conn clientVer;
+            reply Ok
+          end else
+            error ("Client RPC version not supported. "
+                   ^ "Version received from client: \""
+                   ^ string_of_int clientVer ^ "\". "
+                   ^ "Supported RPC versions: " ^ rpcSupportedVersionStr)
+      | None -> Lwt.return ()
+      | exception Util.Transient e -> error e
+
+(****)
+
 let showWarningOnClient =
     (registerServerCmd
-       "showWarningOnClient"
+       "showWarningOnClient" Umarshal.string Umarshal.unit
        (fun _ str -> Lwt.return (Util.warn str)))
 
 let forwardMsgToClient =
     (registerServerCmd
-       "forwardMsgToClient"
+       "forwardMsgToClient" Trace.mmsg Umarshal.unit
        (fun _ str -> (*msg "forwardMsgToClient: %s\n" str; *)
           Lwt.return (Trace.displayMessageLocally str)))
 
+(* Compatibility mode for 2.51 clients. *)
+let compatServerInit mode conn =
+  let compatConnectionHeader =
+    match mode with
+    | Some "2.48" -> compat248ConnectionHeader
+    | _ -> compatConnectionHeader
+  in
+  dump conn [(Bytearray.of_string compatConnectionHeader, 0,
+                String.length compatConnectionHeader)] >>= fun () ->
+  (* Send the magic string to notify new clients *)
+  dumpUrgent conn magic >>= fun () ->
+  (* Must enable flow control because that is the default for 2.51.
+     This must be done after dumpUrgent above to ensure that the write
+     token is sent the last. *)
+  enableFlowControl conn true >>= fun () ->
+  (* Let's see if the client noticed the magic string. This is
+     a no-op for old clients. *)
+  checkForMagicString conn
+
+let compatServerRun conn =
+  (* Set the local warning printer to make an RPC to the client and
+     show the warning there; ditto for the message printer *)
+  Util.warnPrinter :=
+    Some (fun str -> Lwt_unix.run (showWarningOnClient conn str));
+  Trace.messageForwarder :=
+    Some (fun str -> Lwt_unix.run (forwardMsgToClient conn str));
+  receive conn >>=
+  Lwt.wait
+
 (* This function loops, waits for commands, and passes them to
    the relevant functions. *)
-let commandLoop in_ch out_ch =
+let commandLoop ~compatMode in_ch out_ch =
   Trace.runningasserver := true;
   (* Send header indicating to the client that it has successfully
      connected to the server *)
-  let conn = setupIO true in_ch out_ch in
+  let conn = makeConnection true in_ch out_ch in
   Lwt.catch
-    (fun e ->
-       dump conn [(Bytearray.of_string connectionHeader, 0,
-                   String.length connectionHeader)]
-         >>= (fun () ->
+    (fun () ->
+       (if compatMode <> None then
+         let () = setConnectionVersion conn 0 in
+         compatServerInit compatMode conn >>= (fun upgrade ->
+         if upgrade then begin
+           (* Restore the state before starting protocol negotiation *)
+           allowWrites conn.outputQueue;
+           disableFlowControl conn.outputQueue
+         end;
+         Lwt.return upgrade)
+       else
+         Lwt.return true) >>= fun upgrade ->
+       debug (fun () -> Util.msg "%sGoing to attempt RPC version upgrade\n"
+                 (if upgrade then "" else "NOT "));
+       if not upgrade then
+         compatServerRun conn
+       else
+       sendStrings conn [connectionHeader; rpcVersionsStr] >>=
+       checkClientVersion conn >>= fun () ->
+       (* From this moment forward, the RPC version has been selected. All
+          communication must now adhere to that version's specification. *)
+       (* Flow control was disabled for RPC version handshake. Enable it
+          for flow control negotiation. *)
+       enableFlowControl conn true >>= (fun () ->
        (* Set the local warning printer to make an RPC to the client and
           show the warning there; ditto for the message printer *)
        Util.warnPrinter :=
@@ -1517,7 +2279,8 @@ let commandLoop in_ch out_ch =
 
 let killServer =
   Prefs.createBool "killserver" false
-    "!kill server when done (even when using sockets)"
+    ~category:(`Advanced `Remote)
+    "kill server when done (even when using sockets)"
     ("When set to \\verb|true|, this flag causes Unison to kill the remote "
      ^ "server process when the synchronization is finished.  This behavior "
      ^ "is the default for \\verb|ssh| connections, so this preference is not "
@@ -1530,55 +2293,119 @@ let killServer =
 (* For backward compatibility *)
 let _ = Prefs.alias killServer "killServer"
 
+(* FIX: This code should be removed when removing 2.51-compatibility code. *)
+let is248Exe =
+  let exeName = Filename.basename (Sys.executable_name) in
+  String.length exeName >= 11 && String.sub exeName 0 11 = "unison-2.48"
+
+let rec accept_retry l =
+  Lwt.catch
+    (fun () -> Lwt_unix.accept l)
+    (function
+     (* Temporary and connection-specific errors *)
+     | Unix.Unix_error (Unix.ECONNABORTED, _, _)
+     | Unix.Unix_error (Unix.EPERM, _, _)  (* Linux firewall *)
+     | Unix.Unix_error
+         (* Resource exhaustion: could be considered temporary *)
+         (Unix.(EMFILE | ENFILE | ENOBUFS | ENOMEM), _, _)
+       (* Linux curiosity: accept(2) may return errors on the new socket *)
+     | Unix.Unix_error (Unix.ENETUNREACH, _, _)
+     | Unix.Unix_error (Unix.EHOSTUNREACH, _, _)
+     | Unix.Unix_error (Unix.ENETDOWN, _, _)
+     | Unix.Unix_error (Unix.EHOSTDOWN, _, _)
+     | Unix.Unix_error (Unix.ETIMEDOUT, _, _) as e ->
+         let errmsg = match e with
+           | Unix.Unix_error (err, _, _) -> Unix.error_message err
+           | _ -> Printexc.to_string e in
+         Util.msg "server: continuing after receiving an error \
+           when accepting client connection: %s\n" errmsg;
+         accept_retry l
+     (* Permanent errors *)
+     | e -> Lwt.fail e)
+
 (* Used by the socket mechanism: Create a socket on portNum and wait
    for a request. Each request is processed by commandLoop. When a
    session finishes, the server waits for another request. *)
-let waitOnPort hostOpt port =
+let waitOnPort hosts port =
   Util.convertUnixErrorsToFatal "waiting on port"
     (fun () ->
-       let host =
-         match hostOpt with
-           Some host -> host
-         | None      -> ""
-       in
-       let listening = Lwt_unix.run (buildListenSocket host port) in
+       let hosts = match hosts with [] -> [""] | _ -> hosts in
+       let listening = Lwt_unix.run (buildListenSocket hosts port) in
        let accepting = Array.make (Safelist.length listening) None in
-       let accept i l =
+       let rec accept i l =
          match accepting.(i) with
            | None ->
-               let st = Lwt_unix.accept l >>= fun s -> Lwt.return (i, s) in
+               let st = accept_retry l >>= fun s -> Lwt.return (i, s) in
                let () = accepting.(i) <- Some st in
                st
            | Some st -> st
-       and serve (i, s) = accepting.(i) <- None; s in
+       and serve (i, s) = accepting.(i) <- None; setKeepalive s; s
+       and setKeepalive = function
+         | (_, Unix.ADDR_UNIX _) -> ()
+         | (c, ADDR_INET _) -> Lwt_unix.setsockopt c Unix.SO_KEEPALIVE true
+       in
        Util.msg "server started\n";
        let rec handleClients () =
          let (connected, _) =
            serve @@ Lwt_unix.run (Lwt.choose (List.mapi accept listening))
          in
-         Lwt_unix.setsockopt connected Unix.SO_KEEPALIVE true;
+         registerIOClose (connected, connected) (fun () -> doCleanup connected);
          begin try
            (* Accept a connection *)
-           Lwt_unix.run (commandLoop connected connected)
+           let compatMode = Some (if is248Exe then "2.48" else "2.51") in
+           Lwt_unix.run (commandLoop ~compatMode connected connected)
          with Util.Fatal "Lost connection with the server" -> () end;
          (* The client has closed its end of the connection *)
-         begin try Lwt_unix.close connected with Unix.Unix_error _ -> () end;
          if not (Prefs.read killServer) then handleClients ()
+       and doCleanup socket =
+         begin try Lwt_unix.close socket with Unix.Unix_error _ -> () end;
+         if not (Prefs.read killServer) then runConnCloseHandlers true
        in
-       handleClients ())
+       try
+         Sys.catch_break true;
+         handleClients ();
+         unixSocketCleanup ()
+       with
+       | Sys.Break ->
+           unixSocketCleanup ()
+       | (Util.Fatal _ | Unix.Unix_error _) as e ->
+           unixSocketCleanup ();
+           raise e
+    )
 
 let beAServer () =
   begin try
     let home = System.getenv "HOME" in
     Util.convertUnixErrorsToFatal
       "changing working directory"
-      (fun () -> System.chdir (System.fspathFromString home))
+      (fun () -> System.chdir home)
   with Not_found ->
     Util.msg
       "Environment variable HOME unbound: \
        executing server in current directory\n"
   end;
+  (* Let's start with 2.51-compatibility mode. Newer clients will add
+     a special override keyword in server args that will disable the
+     compatibility mode.
+
+     FIX: It is a bit of a hack, so better not make it permanent.
+     It was added in 2021 and should be removed after a couple of years. *)
+  let compatMode =
+     try
+       not (Prefs.scanCmdLine "" |> Util.StringMap.find "rest"
+            |> Safelist.mem rpcServerCmdlineOverride)
+     with Not_found -> true
+  in
+  (* Additionally, do a best effort emulation of 2.48.
+     FIX: remove together with code above. *)
+  let compatMode =
+    match compatMode with
+    | true when is248Exe -> Some "2.48"
+    | true -> Some "2.51"
+    | false -> None
+  in
+  begin end;
   Lwt_unix.run
-    (commandLoop
+    (commandLoop ~compatMode
        (Lwt_unix.of_unix_file_descr Unix.stdin)
        (Lwt_unix.of_unix_file_descr Unix.stdout))
diff --git a/src/remote.mli b/src/remote.mli
index 03b7665..8c7ec22 100644
--- a/src/remote.mli
+++ b/src/remote.mli
@@ -5,16 +5,45 @@ module Thread : sig
   val unwindProtect : (unit -> 'a Lwt.t) -> (exn -> unit Lwt.t) -> 'a Lwt.t
 end
 
+(* A pair of functions enabling conversion from type 'a to a 2.51-compatible
+   type and the other way around.
+   The conversion functions are needed because the 2.51-compatible types must
+   be frozen in time and never changed in future. Type 'a can and will change
+   in time as enhancements are added and old code is removed.
+   When a type is changed, breaking compatibility with 2.51, then respective
+   conversion functions must also be added. *)
+type 'a convV0Fun
+val makeConvV0FunArg :
+    ('a -> 'compat)
+ -> ('compat -> 'a)
+ -> 'a convV0Fun * 'b convV0Fun
+val makeConvV0FunRet :
+    ('b -> 'compat)
+ -> ('compat -> 'b)
+ -> 'a convV0Fun * 'b convV0Fun
+val makeConvV0Funs :
+    ('a -> 'compata)
+ -> ('compata -> 'a)
+ -> ('b -> 'compatb)
+ -> ('compatb -> 'b)
+ -> 'a convV0Fun * 'b convV0Fun
+
 (* Register a server function.  The result is a function that takes a host
    name as argument and either executes locally or else communicates with a
    remote server, as appropriate.  (Calling registerServerCmd also has the
    side effect of registering the command under the given name, so that when
    we are running as a server it can be looked up and executed when
    requested by a remote client.) *)
+(* It is not recommended to use this function in new code unless the cmd is
+   truly independent of any roots/replicas. Use [registerRootCmd] or one of
+   the other functions instead. *)
 val registerHostCmd :
     string              (* command name *)
+ -> ?convV0: 'a convV0Fun * 'b convV0Fun
+                        (* 2.51-compatibility functions for args and result *)
+ -> 'a Umarshal.t -> 'b Umarshal.t
  -> ('a -> 'b Lwt.t)    (* local command *)
- -> (   string          (* -> host *)
+ -> (   Common.root     (* -> host (the root path is ignored) *)
      -> 'a              (*    arguments *)
      -> 'b Lwt.t)       (*    -> (suspended) result *)
 
@@ -27,6 +56,10 @@ val registerHostCmd :
    <funcName>OnRoot and <funcName>Local *)
 val registerRootCmd :
     string                         (* command name *)
+ -> ?convV0: (Fspath.t * 'a) convV0Fun * 'b convV0Fun
+                                   (* 2.51-compatibility functions for args
+                                      and result *)
+ -> 'a Umarshal.t -> 'b Umarshal.t
  -> ((Fspath.t * 'a) -> 'b Lwt.t)  (* local command *)
  -> (   Common.root                (* -> root *)
      -> 'a                         (*    additional arguments *)
@@ -41,7 +74,7 @@ val commandAvailable :
 (* Enter "server mode", reading and processing commands from a remote
    client process until killed *)
 val beAServer : unit -> unit
-val waitOnPort : string option -> string -> unit
+val waitOnPort : string list -> string -> unit
 
 (* Whether the server should be killed when the client terminates *)
 val killServer : bool Prefs.t
@@ -49,13 +82,19 @@ val killServer : bool Prefs.t
 (* Establish a connection to the remote server (if any) corresponding
    to the root and return the canonical name of the root *)
 val canonizeRoot :
-  string -> Clroot.clroot -> (string -> string -> string) option ->
+  string -> Clroot.clroot -> (string -> Terminal.termInteract) option ->
   Common.root Lwt.t
 
 (* Test if connection to the remote server (if any) corresponding
    to the root is established. Always returns true for local roots *)
 val isRootConnected : Common.root -> bool
 
+(* Close the connection to server and run all cleanup and [at_conn_close]
+   handlers. Can also be called for a local root; in this case only the
+   cleanup and [at_conn_close] handlers are run (as there is no connection
+   to close). *)
+val clientCloseRootConnection : Common.root -> unit
+
 (* Statistics *)
 val emittedBytes : float ref
 val receivedBytes : float ref
@@ -87,28 +126,24 @@ module MsgIdMap : Map.S with type key = msgId
 val newMsgId : unit -> msgId
 
 type connection
-val connectionToRoot : Common.root -> connection
+val connectionVersion : connection -> int
+val connectionOfRoot : Common.root -> connection
 
 val registerServerCmd :
-  string -> (connection -> 'a -> 'b Lwt.t) -> connection -> 'a -> 'b Lwt.t
-val registerSpecialServerCmd :
-  string ->
-  ('a ->
-   (Bytearray.t * int * int) list -> (Bytearray.t * int * int) list * int) *
-  (Bytearray.t -> int -> 'a) ->
-  ('b ->
-   (Bytearray.t * int * int) list -> (Bytearray.t * int * int) list * int) *
-  (Bytearray.t -> int -> 'b) ->
-  (connection -> 'a -> 'b Lwt.t) -> connection -> 'a -> 'b Lwt.t
-val defaultMarshalingFunctions :
-  ('a ->
-   (Bytearray.t * int * int) list -> (Bytearray.t * int * int) list * int) *
-  (Bytearray.t -> int -> 'b)
+  string
+ -> ?convV0: 'a convV0Fun * 'b convV0Fun
+ -> 'a Umarshal.t -> 'b Umarshal.t
+ -> (connection -> 'a -> 'b Lwt.t)
+ -> connection -> 'a -> 'b Lwt.t
 val intSize : int
 val encodeInt : int -> Bytearray.t * int * int
 val decodeInt : Bytearray.t -> int -> int
 val registerRootCmdWithConnection :
     string                          (* command name *)
+ -> ?convV0: 'a convV0Fun * 'b convV0Fun
+                                    (* 2.51-compatibility functions for args
+                                       and result *)
+ -> 'a Umarshal.t -> 'b Umarshal.t
  -> (connection -> 'a -> 'b Lwt.t)  (* local command *)
  ->    Common.root                  (* root on which the command is executed *)
     -> Common.root                  (* other root *)
@@ -119,8 +154,53 @@ val streamingActivated : bool Prefs.t
 
 val registerStreamCmd :
   string ->
-  ('a ->
+  (connection -> 'a ->
    (Bytearray.t * int * int) list -> (Bytearray.t * int * int) list * int) *
-  (Bytearray.t -> int -> 'a) ->
+  (connection -> Bytearray.t -> int -> 'a) ->
   (connection -> 'a -> unit) ->
   connection -> (('a -> unit Lwt.t) -> 'b Lwt.t) -> 'b Lwt.t
+
+(* Register a function to be run when the connection between client and server
+   is closed (willingly or unexpectedly). The function should not raise
+   exceptions. If it does then running some of the other registered functions
+   may be skipped (which may not be an issue as the exception is likely going
+   to quit the process).
+
+   Registered functions are only expected to be useful when the connection is
+   closed but the process keeps running (a socket server, for example). Do not
+   use it as a substitute for [at_exit].
+
+   These functions are additionally run when "closing" a local sync when there
+   is no actual connection.
+
+   Keep in mind that a function registered like this can be called immediately
+   when a lost connection is detected, before any exception indicating lost
+   connection is raised. *)
+val at_conn_close : ?only_server:bool -> (unit -> unit) -> unit
+
+(* Register resources to be cleaned up when the connection between client and
+   server closes (normally or exceptionally). This cleanup is additionally run
+   when "closing" a local sync when there is no actual connection.
+
+   Closing the resources is still the responsibility of the code opening the
+   resources but it is not always possible to run the resource cleanup code
+   (due to an Lwt thread being stopped, for example). In those cases the
+   registered resources are cleaned up when the connection is closed, as a
+   last resort.
+
+   The returned functions must be used to track the resources registered for
+   cleanup. *)
+type ('a, 'b, 'c) resourceC =
+  { register : 'a -> 'a;       (* Register an opened resource for cleanup *)
+    release : 'a -> 'b;        (* Unregister and close the resource normally *)
+    release_noerr : 'a -> 'c } (* Same as above; don't raise exceptions *)
+
+val resourceWithConnCleanup :
+     ('a -> 'b) (* Function to close the resource normally *)
+  -> ('a -> 'c) (* Function to close the resource, don't raise exceptions *)
+  -> ('a, 'b, 'c) resourceC (* Functions to track resources for cleanup *)
+
+(* Make an [Lwt_util.region] which is automatically purged and reset when
+   the connection between client and server closes. This cleanup is also
+   run when "closing" a local sync when there is no actual connection. *)
+val lwtRegionWithConnCleanup : int -> Lwt_util.region ref
diff --git a/src/sortri.ml b/src/sortri.ml
index 873b917..a8b608b 100644
--- a/src/sortri.ml
+++ b/src/sortri.ml
@@ -24,7 +24,8 @@ let dbgsort = Util.debug "sort"
 
 let bysize =
   Prefs.createBool "sortbysize" false
-    "!list changed files by size, not name"
+    ~category:(`Advanced `Syncprocess)
+    "list changed files by size, not name"
     ("When this flag is set, the user interface will list changed files "
      ^ "by size (smallest first) rather than by name.  This is useful, for "
      ^ "example, for synchronizing over slow links, since it puts very "
@@ -37,13 +38,15 @@ let bysize =
 
 let newfirst =
   Prefs.createBool "sortnewfirst" false
-    "!list new before changed files"
+    ~category:(`Advanced `Syncprocess)
+    "list new before changed files"
     ("When this flag is set, the user interface will list newly created "
      ^ "files before all others.  This is useful, for example, for checking "
      ^ "that newly created files are not `junk', i.e., ones that should be "
      ^ "ignored or deleted rather than synchronized.")
 
-let sortfirst = Pred.create "sortfirst" ~advanced:true
+let sortfirst = Pred.create "sortfirst"
+    ~category:(`Advanced `Syncprocess)
     ("Each argument to \\texttt{sortfirst} is a pattern \\ARG{pathspec}, "
      ^ "which describes a set of paths.  "
      ^ "Files matching any of these patterns will be listed first in the "
@@ -51,7 +54,8 @@ let sortfirst = Pred.create "sortfirst" ~advanced:true
      ^ "The syntax of \\ARG{pathspec} is "
      ^ "described in \\sectionref{pathspec}{Path Specification}.")
 
-let sortlast = Pred.create "sortlast" ~advanced:true
+let sortlast = Pred.create "sortlast"
+    ~category:(`Advanced `Syncprocess)
     ("Similar to \\verb|sortfirst|, except that files matching one of these "
      ^ "patterns will be listed at the very end.")
 
diff --git a/src/stasher.ml b/src/stasher.ml
index b5dec4b..58499d3 100644
--- a/src/stasher.ml
+++ b/src/stasher.ml
@@ -25,7 +25,8 @@ let verbose = Util.debug "stasher+"
 
 let backuplocation =
   Prefs.createString "backuploc" "central"
-    "!where backups are stored ('local' or 'central')"
+    ~category:(`Advanced `Syncprocess)
+    "where backups are stored ('local' or 'central')"
     ("This preference determines whether backups should be kept locally, near the "
      ^ "original files, or"
      ^" in a central directory specified by the \\texttt{backupdir} "
@@ -36,7 +37,8 @@ let backuplocation =
 let _ = Prefs.alias backuplocation "backuplocation"
 
 let backup =
-  Pred.create "backup" ~advanced:true
+  Pred.create "backup"
+    ~category:(`Advanced `Syncprocess)
     ("Including the preference \\texttt{-backup \\ARG{pathspec}} "
      ^ "causes Unison to keep backup files for each path that matches "
      ^ "\\ARG{pathspec}; directories (nor their permissions or any other "
@@ -51,7 +53,8 @@ let backup =
 let _ = Pred.alias backup "mirror"
 
 let backupnot =
-  Pred.create "backupnot" ~advanced:true
+  Pred.create "backupnot"
+    ~category:(`Advanced `Syncprocess)
     ("The values of this preference specify paths or individual files or"
      ^ " regular expressions that should {\\em not} "
      ^ "be backed up, even if the {\\tt backup} preference selects "
@@ -65,13 +68,14 @@ let shouldBackup p =
 
 let backupprefix =
   Prefs.createString "backupprefix" ".bak.$VERSION."
-    "!prefix for the names of backup files"
+    ~category:(`Advanced `Syncprocess)
+    "prefix for the names of backup files"
     ("When a backup for a file \\verb|NAME| is created, it is stored "
      ^ "in a directory specified by \\texttt{backuplocation}, in a file called "
      ^ "\\texttt{backupprefix}\\verb|NAME|\\texttt{backupsuffix}."
      ^ " \\texttt{backupprefix} can include a directory name (causing Unison to "
      ^ "keep all backup files for a given directory in a subdirectory with this name), and both "
-     ^ " \\texttt{backupprefix} and \\texttt{backupsuffix} can contain the string"
+     ^ " \\texttt{backupprefix} and \\texttt{backupsuffix} can contain the string "
      ^ "\\ARG{\\$VERSION}, which will be replaced by the \\emph{age} of the backup "
      ^ "(1 for the most recent, 2 for the second most recent, and so on...)."
      ^ " This keyword is ignored if it appears in a directory name"
@@ -87,12 +91,15 @@ let backupprefix =
 
 let backupsuffix =
   Prefs.createString "backupsuffix" ""
-    "!a suffix to be added to names of backup files"
+    ~category:(`Advanced `Syncprocess)
+    "a suffix to be added to names of backup files"
     ("See \\texttt{backupprefix} for full documentation.")
 
 let backups =
   Prefs.createBool "backups" false
-    "!keep backup copies of all files (see also 'backup')"
+    ~category:(`Advanced `Syncprocess)
+    ~deprecated:true
+    "keep backup copies of all files (see also 'backup')"
     ("Setting this flag to true is equivalent to "
      ^" setting \\texttt{backuplocation} to \\texttt{local}"
      ^" and \\texttt{backup} to \\verb|Name *|.")
@@ -113,7 +120,8 @@ let translateOldPrefs () =
 
 let maxbackups =
   Prefs.createInt "maxbackups" 2
-    "!number of backed up versions of a file"
+    ~category:(`Advanced `Syncprocess)
+    "number of backed up versions of a file"
     ("This preference specifies the number of backup versions that will "
      ^ "be kept by unison, for each path that matches the predicate "
      ^ "\\verb|backup|.  The default is 2.")
@@ -123,7 +131,8 @@ let _ = Prefs.alias maxbackups "backupversions"
 
 let backupdir =
   Prefs.createString "backupdir" ""
-    "!directory for storing centralized backups"
+    ~category:(`Advanced `Syncprocess)
+    "directory for storing centralized backups"
     ("If this preference is set, Unison will use it as the name of the "
      ^ "directory used to store backup files specified by "
      ^ "the {\\tt backup} preference, when {\\tt backuplocation} is set"
@@ -139,22 +148,24 @@ let backupDirectory () =
         if Prefs.read backupdir <> ""
         then Fspath.canonize (Some (Prefs.read backupdir))
         else Fspath.canonize
-               (Some (System.fspathToString (Util.fileInUnisonDir "backup"))))
+               (Some (Util.fileInUnisonDir "backup")))
 
 let backupcurrent =
-  Pred.create "backupcurr" ~advanced:true
+  Pred.create "backupcurr"
+    ~category:(`Advanced `Syncprocess)
     ("Including the preference \\texttt{-backupcurr \\ARG{pathspec}} "
      ^" causes Unison to keep a backup of the {\\em current} version of every file "
      ^ "matching \\ARG{pathspec}.  "
      ^" This file will be saved as a backup with version number 000. Such"
      ^" backups can be used as inputs to external merging programs, for instance.  See "
-     ^ "the documentatation for the \\verb|merge| preference."
+     ^ "the documentation for the \\verb|merge| preference."
      ^" For more details, see \\sectionref{merge}{Merging Conflicting Versions}."
      ^"\n\n The syntax of \\ARG{pathspec} is described in "
      ^ "\\sectionref{pathspec}{Path Specification}.")
 
 let backupcurrentnot =
-  Pred.create "backupcurrnot" ~advanced:true
+  Pred.create "backupcurrnot"
+    ~category:(`Advanced `Syncprocess)
    "Exceptions to \\verb|backupcurr|, like the \\verb|ignorenot| preference."
 
 let shouldBackupCurrent p =
@@ -265,19 +276,33 @@ let updateBackupNamingFunctions () =
 
 (*------------------------------------------------------------------------------------*)
 
-let makeBackupName path i =
+let makeBackupName fspath path i =
+  (* In the special case when the root itself is a file, use the root's name
+     as the backup file name. Empty path will break backups.
+     We only check the path being empty, and not its type, because the root
+     can change from file to dir and vice versa between syncs. *)
+  let path' =
+    if Path.isEmpty path then
+      Path.fromString (Filename.basename (Fspath.toString fspath))
+    else path in
+
   (* if backups are kept centrally, the current version has exactly
      the same name as the original, for convenience. *)
   if i=0 && Prefs.read backuplocation = "central" then
-    path
+    path'
   else
     Path.addSuffixToFinalName
-      (Path.addPrefixToFinalName path (!make_prefix i))
+      (Path.addPrefixToFinalName path' (!make_prefix i))
       (!make_suffix i)
 
-let stashDirectory fspath =
+let stashDirectory fspath path =
   match Prefs.read backuplocation with
     "central" -> backupDirectory ()
+  | "local" when Path.isEmpty path ->
+      (* Special case when the root itself is a file. Can't use the root
+         as the backup location, which must be a directory. Use the root's
+         parent instead. *)
+      Fspath.canonize (Some (Filename.dirname (Fspath.toString fspath)))
   | "local" -> fspath
   |  _ -> raise (Util.Fatal ("backuplocation preference should be set"
                              ^"to central or local."))
@@ -298,15 +323,15 @@ let showContent typ fspath path =
    sufficiently large number!
 *)
 let backupPath fspath path =
-  let sFspath = stashDirectory fspath in
+  let sFspath = stashDirectory fspath path in
 
-  let rec f path i =
-    let tempPath = makeBackupName path i in
+  let rec f fspath path i =
+    let tempPath = makeBackupName fspath path i in
     verbose (fun () -> Util.msg "backupPath f %s %d\n" (Path.toString path) i);
     if Os.exists sFspath tempPath then
       if i < Prefs.read maxbackups then begin
         verbose (fun () -> Util.msg "need to rename backup file\n");
-        Os.rename "backupPath" sFspath tempPath sFspath (f path (i + 1))
+        Os.rename "backupPath" sFspath tempPath sFspath (f fspath path (i + 1))
       end
       else if i >= Prefs.read maxbackups then
         Os.delete sFspath tempPath;
@@ -317,13 +342,13 @@ let backupPath fspath path =
       "mkdirectories %s %s\n"
          (Fspath.toDebugString sFspath) (Path.toString backdir));
     if not (Os.exists sFspath Path.empty) then
-      Os.createDir sFspath Path.empty Props.dirDefault;
+      Os.createDir sFspath Path.empty (Props.perms Props.dirDefault);
     match Path.deconstructRev backdir with
       None -> ()
     | Some (_, parent) ->
         mkdirectories parent;
-        let props = (Fileinfo.get false sFspath Path.empty).Fileinfo.desc in
-        if not (Os.exists sFspath backdir) then Os.createDir sFspath backdir props
+        let perms = Props.perms (Fileinfo.getBasic false sFspath Path.empty).desc in
+        if not (Os.exists sFspath backdir) then Os.createDir sFspath backdir perms
         else (* Do not just check with Os.exists. It must also be a directory.
                 https://github.com/bcpierce00/unison/issues/30
                 If a non-directory with the same name exists, it must be moved
@@ -332,13 +357,13 @@ let backupPath fspath path =
                 This is only applicable with backuplocation "central" as it
                 will create a separate directory tree. *)
         if (Prefs.read backuplocation = "central") &&
-          (Fileinfo.get false sFspath backdir).Fileinfo.typ != `DIRECTORY then
-          let backdir = f backdir 0 in
-          Os.createDir sFspath backdir props in
+          Fileinfo.getType false sFspath backdir != `DIRECTORY then
+          let backdir = f sFspath backdir 0 in
+          Os.createDir sFspath backdir perms in
 
-  let path0 = makeBackupName path 0 in
-  let sourceTyp = (Fileinfo.get true fspath path).Fileinfo.typ in
-  let path0Typ = (Fileinfo.get false sFspath path0).Fileinfo.typ in
+  let path0 = makeBackupName fspath path 0 in
+  let sourceTyp = Fileinfo.getType true fspath path in
+  let path0Typ = Fileinfo.getType false sFspath path0 in
 
   if   (   sourceTyp = `FILE && path0Typ = `FILE
        && (Fingerprint.file fspath path) = (Fingerprint.file sFspath path0))
@@ -358,7 +383,7 @@ let backupPath fspath path =
       (showContent path0Typ sFspath path0)
       (Fspath.toDebugString fspath) (Path.toString path)
       (showContent sourceTyp fspath path));
-    let sPath = f path 0 in
+    let sPath = f fspath path 0 in
     (* Make sure the parent directory exists *)
     begin match Path.deconstructRev sPath with
      | None -> mkdirectories Path.empty
@@ -464,14 +489,14 @@ let getRecentVersion fspath path fingerprint =
       (Path.toString path)
       (Fspath.toDebugString fspath));
   Util.convertUnixErrorsToTransient "getRecentVersion" (fun () ->
-    let dir = stashDirectory fspath in
+    let dir = stashDirectory fspath path in
     let rec aux_find i =
-      let path = makeBackupName path i in
+      let path = makeBackupName fspath path i in
       if Os.exists dir path &&
         (* FIX: should check that the existing file has the same size, to
            avoid computing the fingerprint if it is obviously going to be
            different... *)
-        (let dig = Os.fingerprint dir path (Fileinfo.get false dir path) in
+        (let dig = Os.fingerprint dir path (Fileinfo.getType false dir path) in
 	 dig = fingerprint)
       then begin
         debug (fun () ->
@@ -502,7 +527,7 @@ let initBackupsLocal () =
 
 let initBackupsRoot: Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "initBackups"
+    "initBackups" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) ->
       Lwt.return (initBackupsLocal ()))
 
diff --git a/src/strings.ml b/src/strings.ml
index 3537c7d..6e853eb 100644
--- a/src/strings.ml
+++ b/src/strings.ml
@@ -4,7 +4,7 @@
 let docs =
     ("about", ("About Unison", 
      "Unison File Synchronizer\n\
-      Version 2.51.5\n\
+      Version 2.53.3\n\
       \n\
       "))
 ::
@@ -62,7 +62,7 @@ let docs =
       \032  Benjamin Pierce (http://www.cis.upenn.edu/~bcpierce/) leads the Unison\n\
       \032  project. The current version of Unison was designed and implemented by\n\
       \032  Trevor Jim (http://www.research.att.com/~trevor/), Benjamin Pierce\n\
-      \032  (http://www.cis.upenn.edu/~bcpierce/), and J\233r\244me Vouillon\n\
+      \032  (http://www.cis.upenn.edu/~bcpierce/), and J\195\169r\195\180me Vouillon\n\
       \032  (http://www.pps.jussieu.fr/~vouillon/), with Alan Schmitt\n\
       \032  (http://alan.petitepomme.net/), Malo Denielou, Zhe Yang\n\
       \032  (http://www.brics.dk/~zheyang/), Sylvain Gommier, and Matthieu Goulay.\n\
@@ -70,11 +70,11 @@ let docs =
       \032  improved by Ben Willmore. Our implementation of the rsync\n\
       \032  (http://samba.org/rsync/) protocol was built by Norman Ramsey\n\
       \032  (http://www.eecs.harvard.edu/~nr/) and Sylvain Gommier. It is based on\n\
-      \032  Andrew Tridgell (http://samba.anu.edu.au/~tridge/)'s thesis work\n\
+      \032  Andrew Tridgell (http://samba.anu.edu.au/~tridge/)\226\128\153s thesis work\n\
       \032  (http://samba.anu.edu.au/~tridge/phd_thesis.pdf) and inspired by his\n\
       \032  rsync (http://samba.org/rsync/) utility. The mirroring and merging\n\
       \032  functionality was implemented by Sylvain Roy, improved by Malo\n\
-      \032  Denielou, and improved yet further by St\233phane Lescuyer. Jacques\n\
+      \032  Denielou, and improved yet further by St\195\169phane Lescuyer. Jacques\n\
       \032  Garrigue (http://wwwfun.kurims.kyoto-u.ac.jp/~garrigue/) contributed\n\
       \032  the original Gtk version of the user interface; the Gtk2 version was\n\
       \032  built by Stephen Tse. Sundar Balasubramaniam helped build a prototype\n\
@@ -132,7 +132,7 @@ let docs =
       \032  small improvements, and contributed patches.\n\
       \n\
       \032  Proposed changes to unison are welcome. They should be submitted as\n\
-      \032  pull requests. (Since safety and robustness are Unison's most important\n\
+      \032  pull requests. (Since safety and robustness are Unison\226\128\153s most important\n\
       \032  properties, patches will be held to high standards of clear design and\n\
       \032  clean coding.) If you want to contribute to Unison, start by\n\
       \032  downloading the developer tarball from the download page. For some\n\
@@ -166,7 +166,7 @@ let docs =
       \n\
       \032  Work on Unison has been supported by the National Science Foundation\n\
       \032  under grants CCR-9701826 and ITR-0113226, Principles and Practice of\n\
-      \032  Synchronization, and by University of Pennsylvania's Institute for\n\
+      \032  Synchronization, and by University of Pennsylvania\226\128\153s Institute for\n\
       \032  Research in Cognitive Science (IRCS).\n\
       \n\
       "))
@@ -190,7 +190,7 @@ let docs =
       \032  You will need to install a copy of Unison on every machine that you\n\
       \032  want to synchronize. However, you only need the version with a\n\
       \032  graphical user interface (if you want a GUI at all) on the machine\n\
-      \032  where you're actually going to display the interface (the CLIENT\n\
+      \032  where you\226\128\153re actually going to display the interface (the CLIENT\n\
       \032  machine). Other machines that you synchronize with can get along just\n\
       \032  fine with the textual version.\n\
       \n\
@@ -198,10 +198,10 @@ let docs =
       \n\
       \032  See https://github.com/bcpierce00/unison/wiki/Downloading-Unison.\n\
       \n\
-      \032  If a pre-built binary of Unison is available for the client machine's\n\
+      \032  If a pre-built binary of Unison is available for the client machine\226\128\153s\n\
       \032  architecture, just download it and put it somewhere in your search path\n\
-      \032  (if you're going to invoke it from the command line) or on your desktop\n\
-      \032  (if you'll be click-starting it).\n\
+      \032  (if you\226\128\153re going to invoke it from the command line) or on your desktop\n\
+      \032  (if you\226\128\153ll be click-starting it).\n\
       \n\
       \032  The executable file for the graphical version (with a name including\n\
       \032  gtkui) actually provides both interfaces: the graphical one appears by\n\
@@ -209,23 +209,22 @@ let docs =
       \032  text on the command line. The textui executable provides just the\n\
       \032  textual interface.\n\
       \n\
-      \032  If you don't see a pre-built executable for your architecture, you'll\n\
-      \032  need to build it yourself. See the section \"Building Unison from\n\
-      \032  Scratch\" .\n\
+      \032  If you don\226\128\153t see a pre-built executable for your architecture, you\226\128\153ll\n\
+      \032  need to build it yourself. See the section \226\128\156Building Unison from\n\
+      \032  Scratch\226\128\157 .\n\
       \n\
       \032  Check to make sure that what you have downloaded is really executable.\n\
       \032  Either click-start it, or type \"unison -version\" at the command line.\n\
       \n\
       \032  Unison can be used in three different modes: with different directories\n\
-      \032  on a single machine, with a remote machine over a direct socket\n\
-      \032  connection, or with a remote machine using ssh for authentication and\n\
-      \032  secure transfer. If you intend to use the last option, you may need to\n\
-      \032  install ssh; see the section \"Installing Ssh\" .\n\
+      \032  on a single machine, with a local or a remote machine over a direct\n\
+      \032  socket connection, or with a remote machine using ssh for\n\
+      \032  authentication and secure transfer.\n\
       \n\
       Running Unison\n\
       \n\
-      \032  Once you've got Unison installed on at least one system, read the\n\
-      \032  section \"Tutorial\" of the user manual (or type \"unison -doc tutorial\")\n\
+      \032  Once you\226\128\153ve got Unison installed on at least one system, read the\n\
+      \032  section \226\128\156Tutorial\226\128\157 of the user manual (or type \"unison -doc tutorial\")\n\
       \032  for instructions on how to get started.\n\
       \n\
       Upgrading\n\
@@ -238,11 +237,78 @@ let docs =
       \032  version of Unison will sometimes introduce a different format for the\n\
       \032  archive files used to remember information about the previous state of\n\
       \032  the replicas. In this case, the old archive will be ignored (not\n\
-      \032  deleted -- if you roll back to the previous version of Unison, you will\n\
+      \032  deleted \226\128\148 if you roll back to the previous version of Unison, you will\n\
       \032  find the old archives intact), which means that any differences between\n\
       \032  the replicas will show up as conflicts that need to be resolved\n\
       \032  manually.\n\
       \n\
+      \032  As of version 2.52, Unison has a degree of backward and forward\n\
+      \032  compatibility. This means three things. First, it is possible for local\n\
+      \032  and remote machines to run a different version of Unison. Second, it is\n\
+      \032  possible for local and remote machines to run a version (same or\n\
+      \032  different) of Unison built with a different version of OCaml compiler\n\
+      \032  (this has been problematic historically). Lastly, it is possible to\n\
+      \032  upgrade Unison on the local machine (compiled with any OCaml version)\n\
+      \032  and keep the existing archive.\n\
+      \n\
+      \032  If version interoperability requirements are followed then Unison 2.52\n\
+      \032  and newer can upgrade the archive created by earlier Unison versions.\n\
+      \032  To avoid rebuilding archive files when upgrading from a version older\n\
+      \032  than 2.52, you must install version 2.52 or newer built with the same\n\
+      \032  OCaml version as your previous version of Unison, and then run it at\n\
+      \032  least once on each root. Doing so will upgrade the archive file.\n\
+      \n\
+      \032  After upgrading the archive, you are free to swap the Unison 2.52 or\n\
+      \032  newer executable to one compiled with a different version of OCaml. The\n\
+      \032  archive file is no longer dependent on the compiler version.\n\
+      \n\
+      Version interoperability\n\
+      \n\
+      \032  To ensure interoperability with different Unison versions on local and\n\
+      \032  remote machines, and to upgrade from an earlier version without\n\
+      \032  rebuilding the archive files, you have to remember these guidelines.\n\
+      \032  Upgrading from an incompatible version, while possible and normal, will\n\
+      \032  require fully scanning both roots, which can be time-consuming with big\n\
+      \032  replicas.\n\
+      \n\
+      \032  Unison 2.52 and newer are compatible with:\n\
+      \032    * Unison 2.52 or newer (for as long as backwards compatibility is\n\
+      \032      maintained in the newer versions). You do not have to pay any\n\
+      \032      attention to OCaml compiler versions.\n\
+      \032    * Unison 2.51 if both versions are compiled with same OCaml compiler\n\
+      \032      version (you can see which compiler version was used by running\n\
+      \032      unison -version).\n\
+      \032    * Unison 2.48 if both versions are compiled with same OCaml compiler\n\
+      \032      version. See special notes below.\n\
+      \n\
+      \032  Interoperability matrix for quick reference:\n\
+      \n\
+      \032  Client versions Server versions\n\
+      \032                  2.52 or newer       2.51               2.48\n\
+      \032  2.52 or newer   full interop        same OCaml version same OCaml version\n\
+      \032  2.51            same OCaml version  full interop       no interop\n\
+      \032  2.48            same OCaml version* no interop         full interop\n\
+      \n\
+      \032  Special notes for Unison 2.48:\n\
+      \032    * Unison 2.48 does not show which OCaml compiler was used to compile\n\
+      \032      it. If you do not have the option of re-compiling the 2.48 version,\n\
+      \032      you have two alternatives. First (and most likely to succeed), see\n\
+      \032      what is the version of the OCaml compiler in the same package\n\
+      \032      repository where you installed Unison 2.48 from, then use Unison\n\
+      \032      2.52 compiled with that version. Second, you can just try Unison\n\
+      \032      2.52 executables compiled with different OCaml versions and see\n\
+      \032      which one works with your copy of Unison 2.48.\n\
+      \032    * When running Unison 2.48 on the client machine with Unison 2.52 or\n\
+      \032      newer on the server machine, you have to do some additional\n\
+      \032      configuration. The Unison executable name on the server must start\n\
+      \032      with unison-2.48 (just unison-2.48 is ok, as is unison-2.48.exe,\n\
+      \032      but also unison-2.48+ocaml-4.05). If using TCP socket connection to\n\
+      \032      the server then you\226\128\153re all set! If using ssh then you have to add\n\
+      \032      one of the following options to your profile or as a command-line\n\
+      \032      argument on the client machine: -addversionno; see the section\n\
+      \032      \226\128\156Remote Usage\226\128\157 , or -servercmd; see the section \226\128\156Remote Shell\n\
+      \032      Method\226\128\157 .\n\
+      \n\
       Building Unison from Scratch\n\
       \n\
       \032  If a pre-built image is not available, you will need to compile it from\n\
@@ -253,99 +319,7 @@ let docs =
       \032  been tested on many flavors of Windows (98, NT, 2000, XP) and Unix (OS\n\
       \032  X, Solaris, Linux, FreeBSD), and on both 32- and 64-bit architectures.\n\
       \n\
-      Unix\n\
-      \n\
-      \032  Unison can be built with or without a graphical user interface (GUI).\n\
-      \032  The build system will decide automatically depending on the libraries\n\
-      \032  installed on your system, but you can also type make UISTYLE=text to\n\
-      \032  build Unison without GUI.\n\
-      \n\
-      \032  You'll need the Objective Caml compiler, available from\n\
-      \032  http://caml.inria.fr. OCaml is available from most package managers\n\
-      \032  Building and installing OCaml on Unix systems is very straightforward;\n\
-      \032  just follow the instructions in the distribution. You'll probably want\n\
-      \032  to build the native-code compiler in addition to the bytecode compiler,\n\
-      \032  as Unison runs much faster when compiled to native code, but this is\n\
-      \032  not absolutely necessary. (Quick start: on many systems, the following\n\
-      \032  sequence of commands will get you a working and installed compiler:\n\
-      \032  first do make world opt, then su to root and do make install.)\n\
-      \n\
-      \032  You'll also need the GNU make utility, which is standard on most Unix\n\
-      \032  systems. Unison's build system is not parallelizable, so don't use\n\
-      \032  flags that cause it to start processes in parallel (e.g. -j).\n\
-      \n\
-      \032  Once you've got OCaml installed, grab a copy of the Unison sources,\n\
-      \032  unzip and untar them, change to the new \"unison\" directory, and type\n\
-      \032  \"make UISTYLE=text\". The result should be an executable file called\n\
-      \032  \"unison\". Type \"./unison\" to make sure the program is executable. You\n\
-      \032  should get back a usage message.\n\
-      \n\
-      \032  If you want to build the graphical user interface, you will need to\n\
-      \032  install some additional things:\n\
-      \032    * The Gtk2 development libraries (package libgtk2.0-dev on debian\n\
-      \032      based systems).\n\
-      \032    * OCaml bindings for Gtk2. Install them from your software\n\
-      \032      repositories (package liblablgtk2-ocaml on debian based systems).\n\
-      \032      Also available from\n\
-      \032      http://wwwfun.kurims.kyoto-u.ac.jp/soft/olabl/lablgtk.html.\n\
-      \032    * Pango, a text rendering library and a part of Gtk2. On some systems\n\
-      \032      (e.g. Ubuntu) the bindings between Pango and OCaml need to be\n\
-      \032      installed explicitly (package liblablgtk-extras-ocaml-dev on\n\
-      \032      Ubuntu).\n\
-      \n\
-      \032  Type make src to build Unison. If Gtk2 is available on the system,\n\
-      \032  Unison with a GUI will be built automatically.\n\
-      \n\
-      \032  Put the unison executable somewhere in your search path, either by\n\
-      \032  adding the Unison directory to your PATH variable or by copying the\n\
-      \032  executable to some standard directory where executables are stored. Or\n\
-      \032  just type make install to install Unison to $HOME/bin/unison.\n\
-      \n\
-      Mac OS X\n\
-      \n\
-      \032  To build the text-only user interface, follow the instructions above\n\
-      \032  for building on Unix systems. You should do this first, even if you are\n\
-      \032  also planning on building the GUI, just to make sure it works.\n\
-      \n\
-      \032  To build the basic GUI version, you'll first need to download and\n\
-      \032  install the XCode developer tools from Apple. Once this is done, just\n\
-      \032  type make in the src directory, and if things go well you should get an\n\
-      \032  application that you can move from uimac/build/Default/Unison.app to\n\
-      \032  wherever you want it.\n\
-      \n\
-      Windows\n\
-      \n\
-      \032  Although the binary distribution should work on any version of Windows,\n\
-      \032  some people may want to build Unison from scratch on those systems too.\n\
-      \n\
-      Bytecode version:\n\
-      \n\
-      \032  The simpler but slower compilation option to build a Unison executable\n\
-      \032  is to build a bytecode version. You need first install Windows version\n\
-      \032  of the OCaml compiler (version 3.07 or later, available from\n\
-      \032  http://caml.inria.fr). Then grab a copy of Unison sources and type\n\
-      \032      make NATIVE=false\n\
-      \n\
-      \032  to compile the bytecode. The result should be an executable file called\n\
-      \032  unison.exe.\n\
-      \n\
-      Native version:\n\
-      \n\
-      \032  Building a more efficient, native version of Unison on Windows requires\n\
-      \032  a little more work. See the file INSTALL.win32 in the source code\n\
-      \032  distribution.\n\
-      \n\
-      Installation Options\n\
-      \n\
-      \032  The Makefile in the distribution includes several switches that can be\n\
-      \032  used to control how Unison is built. Here are the most useful ones:\n\
-      \032    * Building with NATIVE=true uses the native-code OCaml compiler,\n\
-      \032      yielding an executable that will run quite a bit faster. We use\n\
-      \032      this for building distribution versions.\n\
-      \032    * Building with make DEBUGGING=true generates debugging symbols.\n\
-      \032    * Building with make STATIC=true generates a (mostly) statically\n\
-      \032      linked executable. We use this for building distribution versions,\n\
-      \032      for portability.\n\
+      \032  Building instructions are included with the source code.\n\
       \n\
       "))
 ::
@@ -369,29 +343,29 @@ let docs =
       \032  identical.\n\
       \n\
       \032  The graphical version can also be run directly by clicking on its icon,\n\
-      \032  but this may require a little set-up (see the section \"Click-starting\n\
-      \032  Unison\" ). For this tutorial, we assume that you're starting it from\n\
+      \032  but this may require a little set-up (see the section \226\128\156Click-starting\n\
+      \032  Unison\226\128\157 ). For this tutorial, we assume that you\226\128\153re starting it from\n\
       \032  the command line.\n\
       \n\
       \032  Unison can synchronize files and directories on a single machine, or\n\
       \032  between two machines on a network. (The same program runs on both\n\
       \032  machines; the only difference is which one is responsible for\n\
-      \032  displaying the user interface.) If you're only interested in a\n\
-      \032  single-machine setup, then let's call that machine the CLIENT . If\n\
-      \032  you're synchronizing two machines, let's call them CLIENT and SERVER .\n\
+      \032  displaying the user interface.) If you\226\128\153re only interested in a\n\
+      \032  single-machine setup, then let\226\128\153s call that machine the CLIENT . If\n\
+      \032  you\226\128\153re synchronizing two machines, let\226\128\153s call them CLIENT and SERVER .\n\
       \n\
       Local Usage\n\
       \n\
-      \032  Let's get the client machine set up first and see how to synchronize\n\
+      \032  Let\226\128\153s get the client machine set up first and see how to synchronize\n\
       \032  two directories on a single machine.\n\
       \n\
-      \032  Follow the instructions in the section \"Installation\" to either\n\
+      \032  Follow the instructions in the section \226\128\156Installation\226\128\157 to either\n\
       \032  download or build an executable version of Unison, and install it\n\
       \032  somewhere on your search path. (If you just want to use the textual\n\
       \032  user interface, download the appropriate textui binary. If you just\n\
-      \032  want to the graphical interface--or if you will use both interfaces\n\
-      \032  [the gtkui binary actually has both compiled in]--then download the\n\
-      \032  gtkui binary.)\n\
+      \032  want to the graphical interface\226\128\148or if you will use both interfaces [the\n\
+      \032  gtkui binary actually has both compiled in]\226\128\148then download the gtkui\n\
+      \032  binary.)\n\
       \n\
       \032  Create a small test directory a.tmp containing a couple of files and/or\n\
       \032  subdirectories, e.g.,\n\
@@ -404,7 +378,7 @@ let docs =
       \032      cp -r a.tmp b.tmp\n\
       \n\
       \032  Now try synchronizing a.tmp and b.tmp. (Since they are identical,\n\
-      \032  synchronizing them won't propagate any changes, but Unison will\n\
+      \032  synchronizing them won\226\128\153t propagate any changes, but Unison will\n\
       \032  remember the current state of both directories so that it will be able\n\
       \032  to tell next time what has changed.) Type:\n\
       \032      unison a.tmp b.tmp\n\
@@ -440,7 +414,7 @@ let docs =
       \n\
       \032  indicates that the file c has been modified only in the second replica,\n\
       \032  and that the default action is therefore to propagate the new version\n\
-      \032  to the first replica. To follow Unison's recommendation, press the \"f\"\n\
+      \032  to the first replica. To follow Unison\226\128\153s recommendation, press the \226\128\156f\226\128\157\n\
       \032  at the prompt.\n\
       \n\
       \032  If both replicas are modified and their contents are different, then\n\
@@ -456,7 +430,7 @@ let docs =
       \032  shown. Unison simply notes that the file is up to date.\n\
       \n\
       \032  These display conventions are used by both versions of the user\n\
-      \032  interface. The only difference lies in the way in which Unison's\n\
+      \032  interface. The only difference lies in the way in which Unison\226\128\153s\n\
       \032  default actions are either accepted or overridden by the user.\n\
       \n\
       \032  Textual Interface:\n\
@@ -465,13 +439,13 @@ let docs =
       \032      interface will ask for instructions as to how to propagate the\n\
       \032      change. If some default action is indicated (by an arrow), you can\n\
       \032      simply press Return to go on to the next changed file. If you want\n\
-      \032      to do something different with this file, press \"<\" or \">\" to force\n\
+      \032      to do something different with this file, press \226\128\156<\226\128\157 or \226\128\156>\226\128\157 to force\n\
       \032      the change to be propagated from right to left or from left to\n\
-      \032      right, or else press \"/\" to skip this file and leave both replicas\n\
+      \032      right, or else press \226\128\156/\226\128\157 to skip this file and leave both replicas\n\
       \032      alone. When it reaches the end of the list of modified files,\n\
       \032      Unison will ask you one more time whether it should proceed with\n\
       \032      the updates that have been selected.\n\
-      \032      When Unison stops to wait for input from the user, pressing \"?\"\n\
+      \032      When Unison stops to wait for input from the user, pressing \226\128\156?\226\128\157\n\
       \032      will always give a list of possible responses and their meanings.\n\
       \n\
       \032  Graphical Interface:\n\
@@ -479,61 +453,67 @@ let docs =
       \032      either a.tmp or b.tmp. To override a default action (or to select\n\
       \032      an action in the case when there is no default), first select the\n\
       \032      file, either by clicking on its name or by using the up- and\n\
-      \032      down-arrow keys. Then press either the left-arrow or \"<\" key (to\n\
+      \032      down-arrow keys. Then press either the left-arrow or \226\128\156<\226\128\157 key (to\n\
       \032      cause the version in b.tmp to propagate to a.tmp) or the\n\
-      \032      right-arrow or \">\" key (which makes the a.tmp version override\n\
+      \032      right-arrow or \226\128\156>\226\128\157 key (which makes the a.tmp version override\n\
       \032      b.tmp).\n\
       \032      Every keyboard command can also be invoked from the menus at the\n\
       \032      top of the user interface. (Conversely, each menu item is annotated\n\
       \032      with its keyboard equivalent, if it has one.)\n\
       \032      When you are satisfied with the directions for the propagation of\n\
-      \032      changes as shown in the main window, click the \"Go\" button to set\n\
+      \032      changes as shown in the main window, click the \226\128\156Go\226\128\157 button to set\n\
       \032      them in motion. A check sign will be displayed next to each\n\
       \032      filename when the file has been dealt with.\n\
       \n\
       Remote Usage\n\
       \n\
-      \032  Next, we'll get Unison set up to synchronize replicas on two different\n\
+      \032  Next, we\226\128\153ll get Unison set up to synchronize replicas on two different\n\
       \032  machines.\n\
       \n\
+      \032  NB: Unison has not been designed to run with elevated privileges (e.g.\n\
+      \032  setuid), and it has not been audited for that environment. Therefore\n\
+      \032  Unison should be run with the userid of the owner of the files to be\n\
+      \032  synchronized, and should never be run setuid or similar. (Problems\n\
+      \032  encountered when running setuid etc. must be reproduced without setuid\n\
+      \032  before being reported as bugs.)\n\
+      \n\
       \032  Follow the instructions in the Installation section to download or\n\
       \032  build an executable version of Unison on the server machine, and\n\
-      \032  install it somewhere on your search path. (It doesn't matter whether\n\
+      \032  install it somewhere on your search path. (It doesn\226\128\153t matter whether\n\
       \032  you install the textual or graphical version, since the copy of Unison\n\
-      \032  on the server doesn't need to display any user interface at all.)\n\
+      \032  on the server doesn\226\128\153t need to display any user interface at all.)\n\
       \n\
       \032  It is important that the version of Unison installed on the server\n\
       \032  machine is the same as the version of Unison on the client machine. But\n\
       \032  some flexibility on the version of Unison at the client side can be\n\
       \032  achieved by using the -addversionno option; see the section\n\
-      \032  \"Preferences\" .\n\
+      \032  \226\128\156Preferences\226\128\157 .\n\
       \n\
       \032  Now there is a decision to be made. Unison provides two methods for\n\
       \032  communicating between the client and the server:\n\
       \032    * Remote shell method: To use this method, you must have some way of\n\
-      \032      invoking remote commands on the server from the client's command\n\
+      \032      invoking remote commands on the server from the client\226\128\153s command\n\
       \032      line, using a facility such as ssh. This method is more convenient\n\
-      \032      (since there is no need to manually start a \"unison server\" process\n\
-      \032      on the server) and also more secure (especially if you use ssh).\n\
-      \032    * Socket method: This method requires only that you can get TCP\n\
-      \032      packets from the client to the server and back. A draconian\n\
-      \032      firewall can prevent this, but otherwise it should work anywhere.\n\
+      \032      (since there is no need to manually start a \226\128\156unison server\226\128\157 process\n\
+      \032      on the server) and also more secure, assuming you are using ssh).\n\
+      \032    * TCP socket method: This method requires only that you can get TCP\n\
+      \032      packets from the client to the server and back. It is insecure and\n\
+      \032      should not be used.\n\
+      \032    * Unix socket method: This method only works within a single machine.\n\
+      \032      It is similar to the TCP sockets method, but it is possible to\n\
+      \032      configure it securely.\n\
       \n\
       \032  Decide which of these you want to try, and continue with the section\n\
-      \032  \"Remote Shell Method\" or the section \"Socket Method\" , as appropriate.\n\
+      \032  \226\128\156Remote Shell Method\226\128\157 or the section \226\128\156Socket Method\226\128\157 , as appropriate.\n\
       \n\
       Remote Shell Method\n\
       \n\
-      \032  The standard remote shell facility on Unix systems is ssh, which\n\
-      \032  provides the same functionality as the older rsh but much better\n\
-      \032  security. Ssh is available from http://www.openssh.org. See section\n\
-      \032  [1]?? for installation instructions for the Windows version.\n\
+      \032  The standard remote shell facility on Unix systems is ssh.\n\
       \n\
       \032  Running ssh requires some coordination between the client and server\n\
       \032  machines to establish that the client is allowed to invoke commands on\n\
       \032  the server; please refer to the ssh documentation for information on\n\
-      \032  how to set this up. The examples in this section use ssh, but you can\n\
-      \032  substitute rsh for ssh if you wish.\n\
+      \032  how to set this up.\n\
       \n\
       \032  First, test that we can invoke Unison on the server from the client.\n\
       \032  Typing\n\
@@ -543,9 +523,9 @@ let docs =
       \032       unison -version\n\
       \n\
       \032  locally on the client. If remote execution fails, then either something\n\
-      \032  is wrong with your ssh setup (e.g., \"permission denied\") or else the\n\
-      \032  search path that's being used when executing commands on the server\n\
-      \032  doesn't contain the unison executable (e.g., \"command not found\").\n\
+      \032  is wrong with your ssh setup (e.g., \226\128\156permission denied\226\128\157) or else the\n\
+      \032  search path that\226\128\153s being used when executing commands on the server\n\
+      \032  doesn\226\128\153t contain the unison executable (e.g., \226\128\156command not found\226\128\157).\n\
       \n\
       \032  Create a test directory a.tmp in your home directory on the client\n\
       \032  machine.\n\
@@ -579,15 +559,33 @@ let docs =
       \032      server by using the command-line option \"-servercmd\n\
       \032      /full/path/name/of/unison\" or adding\n\
       \032      \"servercmd=/full/path/name/of/unison\" to your profile (see the\n\
-      \032      section \"Profiles\" ). Similarly, you can specify a explicit path\n\
+      \032      section \226\128\156Profiles\226\128\157 ). Similarly, you can specify an explicit path\n\
       \032      for the ssh program using the \"-sshcmd\" option. Extra arguments can\n\
       \032      be passed to ssh by setting the -sshargs preference.\n\
+      \032    * By leveraging \"-sshcmd\" and \"-sshargs\", you can effectively use any\n\
+      \032      remote shell program, not just ssh; just remember that the roots\n\
+      \032      are still specified with ssh as the protocol, that is, they have to\n\
+      \032      start with \"ssh://\".\n\
       \n\
       Socket Method\n\
       \n\
-      \032    Warning: The socket method is insecure: not only are the texts of\n\
-      \032    your changes transmitted over the network in unprotected form, it is\n\
-      \032    also possible for anyone in the world to connect to the server\n\
+      \032  To run Unison over a socket connection, you must start a Unison daemon\n\
+      \032  process on the server. This process runs continuously, waiting for\n\
+      \032  connections over a given socket from client machines running Unison and\n\
+      \032  processing their requests in turn.\n\
+      \n\
+      \032  Since the socket method is not used by many people, its functionality\n\
+      \032  is rather limited. For example, the server can only deal with one\n\
+      \032  client at a time.\n\
+      \n\
+      \032  Note that the Unison daemon process is always started with a\n\
+      \032  command-line argument; not from a profile.\n\
+      \n\
+      TCP Sockets\n\
+      \n\
+      \032    Warning: The TCP socket method is insecure: not only are the texts\n\
+      \032    of your changes transmitted over the network in unprotected form, it\n\
+      \032    is also possible for anyone in the world to connect to the server\n\
       \032    process and read out the contents of your filesystem! (Of course, to\n\
       \032    do this they must understand the protocol that Unison uses to\n\
       \032    communicate between client and server, but all they need for this is\n\
@@ -595,39 +593,58 @@ let docs =
       \032    for expert users with specific needs; everyone else should use the\n\
       \032    ssh method.\n\
       \n\
-      \032  To run Unison over a socket connection, you must start a Unison daemon\n\
-      \032  process on the server. This process runs continuously, waiting for\n\
-      \032  connections over a given socket from client machines running Unison and\n\
-      \032  processing their requests in turn.\n\
-      \n\
-      \032  Note that socket mode cannot be started from a profile. It should be\n\
-      \032  started as a command-line argument only.\n\
-      \n\
-      \032  To start the daemon, type\n\
+      \032  To start the daemon for connections over a TCP socket, type\n\
       \032      unison -socket NNNN\n\
       \n\
-      \032  on the server machine, where NNNN is the socket number that the daemon\n\
-      \032  should listen on for connections from clients. (NNNN can be any large\n\
-      \032  number that is not being used by some other program; if NNNN is already\n\
-      \032  in use, Unison will exit with an error message.) Note that paths\n\
-      \032  specified by the client will be interpreted relative to the directory\n\
-      \032  in which you start the server process; this behavior is different from\n\
-      \032  the ssh case, where the path is relative to your home directory on the\n\
-      \032  server.\n\
+      \032  on the server machine, where NNNN is the TCP port number that the\n\
+      \032  daemon should listen on for connections from clients. (NNNN can be any\n\
+      \032  large number that is not being used by some other program; if NNNN is\n\
+      \032  already in use, Unison will exit with an error message.)\n\
       \n\
       \032  Create a test directory a.tmp in your home directory on the client\n\
       \032  machine. Now type:\n\
       \032      unison a.tmp socket://remotehostname:NNNN/a.tmp\n\
       \n\
-      \032  The result should be that the entire directory a.tmp is propagated from\n\
-      \032  the client to the server (a.tmp will be created on the server in the\n\
-      \032  directory that the server was started from). After finishing the first\n\
-      \032  synchronization, change a few files and try synchronizing again. You\n\
-      \032  should see similar results as in the local case.\n\
-      \n\
-      \032  Since the socket method is not used by many people, its functionality\n\
-      \032  is rather limited. For example, the server can only deal with one\n\
-      \032  client at a time.\n\
+      \032  Note that paths specified by the client will be interpreted relative to\n\
+      \032  the directory in which you start the server process; this behavior is\n\
+      \032  different from the ssh case, where the path is relative to your home\n\
+      \032  directory on the server. The result should be that the entire directory\n\
+      \032  a.tmp is propagated from the client to the server (a.tmp will be\n\
+      \032  created on the server in the directory that the server was started\n\
+      \032  from). After finishing the first synchronization, change a few files\n\
+      \032  and try synchronizing again. You should see similar results as in the\n\
+      \032  local case.\n\
+      \n\
+      \032  By default Unison will listen for incoming connections on all\n\
+      \032  interfaces. If you want to limit this to certain interfaces or\n\
+      \032  addresses then you can use the -listen command-line argument,\n\
+      \032  specifying a host name or an IP address to listen on. -listen can be\n\
+      \032  given multiple times to listen on several addresses.\n\
+      \n\
+      Unix Domain Sockets\n\
+      \n\
+      \032  To start the daemon for connections over a Unix domain socket, type\n\
+      \032      unison -socket PPPP\n\
+      \n\
+      \032  where PPPP is the path to a Unix socket that the daemon should open for\n\
+      \032  connections from clients. (PPPP can be any absolute or relative path\n\
+      \032  the server process has access to but it must not exist yet; the socket\n\
+      \032  is created at that path when the daemon process is started.) You are\n\
+      \032  responsible for securing access to the socket path. For example, this\n\
+      \032  can be done by controlling the permissions of socket\226\128\153s parent\n\
+      \032  directory, or ensuring a restrictive umask value when starting Unison.\n\
+      \n\
+      \032  Clients can connect to a server over a Unix domain socket by specifying\n\
+      \032  the absolute or relative path to the socket, instead of a server\n\
+      \032  address and port number:\n\
+      \032      unison a.tmp socket://{path/to/unix/socket}/a.tmp\n\
+      \n\
+      \032  (socket path is enclosed in curly braces).\n\
+      \n\
+      \032  Note that Unix domain sockets are local sockets (they exist in the\n\
+      \032  filesystem namespace). One could use Unixs socket remotely, by\n\
+      \032  forwarding access to the socket by other means, for example by using\n\
+      \032  spiped secure pipe daemon.\n\
       \n\
       Using Unison for All Your Files\n\
       \n\
@@ -635,7 +652,7 @@ let docs =
       \032  find yourself wanting to use it regularly to synchronize your commonly\n\
       \032  used files. There are several possible ways of going about this:\n\
       \032   1. Synchronize your whole home directory, using the Ignore facility\n\
-      \032      (see the section \"Ignoring Paths\" ) to avoid synchronizing\n\
+      \032      (see the section \226\128\156Ignoring Paths\226\128\157 ) to avoid synchronizing\n\
       \032      temporary files and things that only belong on one host.\n\
       \032   2. Create a subdirectory called shared (or current, or whatever) in\n\
       \032      your home directory on each host, and put all the files you want to\n\
@@ -643,7 +660,7 @@ let docs =
       \032   3. Create a subdirectory called shared (or current, or whatever) in\n\
       \032      your home directory on each host, and put links to all the files\n\
       \032      you want to synchronize into this directory. Use the follow\n\
-      \032      preference (see the section \"Symbolic Links\" ) to make Unison treat\n\
+      \032      preference (see the section \226\128\156Symbolic Links\226\128\157 ) to make Unison treat\n\
       \032      these links as transparent.\n\
       \032   4. Make your home directory the root of the synchronization, but tell\n\
       \032      Unison to synchronize only some of the files and subdirectories\n\
@@ -659,19 +676,19 @@ let docs =
       \032         -path .netscape/bookmarks.html\n\
       \n\
       \032      These -path arguments can also be put in your preference file. See\n\
-      \032      the section \"Preferences\" for an example.\n\
+      \032      the section \226\128\156Preferences\226\128\157 for an example.\n\
       \n\
       \032  Most people find that they only need to maintain a profile (or\n\
       \032  profiles) on one of the hosts that they synchronize, since Unison is\n\
-      \032  always initiated from this host. (For example, if you're synchronizing\n\
-      \032  a laptop with a fileserver, you'll probably always run Unison on the\n\
+      \032  always initiated from this host. (For example, if you\226\128\153re synchronizing\n\
+      \032  a laptop with a fileserver, you\226\128\153ll probably always run Unison on the\n\
       \032  laptop.) This is a bit different from the usual situation with\n\
       \032  asymmetric mirroring programs like rdist, where the mirroring operation\n\
       \032  typically needs to be initiated from the machine with the most recent\n\
-      \032  changes. the section \"Profiles\" covers the syntax of Unison profiles,\n\
+      \032  changes. the section \226\128\156Profiles\226\128\157 covers the syntax of Unison profiles,\n\
       \032  together with some sample profiles.\n\
       \n\
-      \032  Some tips on improving Unison's performance can be found on the\n\
+      \032  Some tips on improving Unison\226\128\153s performance can be found on the\n\
       \032  Frequently Asked Questions page\n\
       \032  (http://www.cis.upenn.edu/~bcpierce/unison/faq.html).\n\
       \n\
@@ -682,11 +699,11 @@ let docs =
       \032  performing multiple pairwise synchronizations.\n\
       \n\
       \032  If you need to do this, the most reliable way to set things up is to\n\
-      \032  organize the machines into a \"star topology,\" with one machine\n\
-      \032  designated as the \"hub\" and the rest as \"spokes,\" and with each spoke\n\
+      \032  organize the machines into a \226\128\156star topology,\226\128\157 with one machine\n\
+      \032  designated as the \226\128\156hub\226\128\157 and the rest as \226\128\156spokes,\226\128\157 and with each spoke\n\
       \032  machine synchronizing only with the hub. The big advantage of the star\n\
-      \032  topology is that it eliminates the possibility of confusing \"spurious\n\
-      \032  conflicts\" arising from the fact that a separate archive is maintained\n\
+      \032  topology is that it eliminates the possibility of confusing \226\128\156spurious\n\
+      \032  conflicts\226\128\157 arising from the fact that a separate archive is maintained\n\
       \032  by Unison for every pair of hosts that it synchronizes.\n\
       \n\
       Going Further\n\
@@ -697,12 +714,12 @@ let docs =
       \n\
       \032  at the command line, or by selecting the Help menu in the graphical\n\
       \032  user interface. The same information is also available in a typeset\n\
-      \032  User's Manual (HTML or PostScript format) through\n\
+      \032  User\226\128\153s Manual (HTML or PostScript format) through\n\
       \032  http://www.cis.upenn.edu/~bcpierce/unison.\n\
       \n\
       \032  If you use Unison regularly, you should subscribe to one of the mailing\n\
       \032  lists, to receive announcements of new versions. See the section\n\
-      \032  \"Mailing Lists and Bug Reporting\" .\n\
+      \032  \226\128\156Mailing Lists and Bug Reporting\226\128\157 .\n\
       \n\
       "))
 ::
@@ -710,14 +727,16 @@ let docs =
      "Basic Concepts\n\
       \n\
       \032  To understand how Unison works, it is necessary to discuss a few\n\
-      \032  straightforward concepts. These concepts are developed more rigorously\n\
-      \032  and at more length in a number of papers, available at\n\
+      \032  straightforward concepts.\n\
+      \n\
+      \032  These concepts are developed more rigorously and at more length in a\n\
+      \032  number of papers, available at\n\
       \032  http://www.cis.upenn.edu/~bcpierce/papers. But the informal\n\
       \032  presentation here should be enough for most users.\n\
       \n\
       Roots\n\
       \n\
-      \032  A replica's root tells Unison where to find a set of files to be\n\
+      \032  A replica\226\128\153s root tells Unison where to find a set of files to be\n\
       \032  synchronized, either on the local machine or on a remote host. For\n\
       \032  example,\n\
       \032     relative/path/of/root\n\
@@ -728,18 +747,23 @@ let docs =
       \n\
       \032  specifies a root relative to the top of the local filesystem,\n\
       \032  independent of where Unison is running. Remote roots can begin with\n\
-      \032  ssh://, rsh:// to indicate that the remote server should be started\n\
-      \032  with rsh or ssh:\n\
+      \032  ssh:// to indicate that the remote server should be started with ssh:\n\
       \032     ssh://remotehost//absolute/path/of/root\n\
-      \032     rsh://user@remotehost/relative/path/of/root\n\
+      \032     ssh://user@remotehost/relative/path/of/root\n\
       \n\
       \032  If the remote server is already running (in the socket mode), then the\n\
       \032  syntax\n\
       \032     socket://remotehost:portnum//absolute/path/of/root\n\
       \032     socket://remotehost:portnum/relative/path/of/root\n\
+      \032     socket://[IPv6literal]:portnum/path\n\
       \n\
       \032  is used to specify the hostname and the port that the client Unison\n\
-      \032  should use to contact it.\n\
+      \032  should use to contact it. Syntax\n\
+      \032     socket://{path/of/socket}//absolute/path/of/root\n\
+      \032     socket://{path/of/socket}/relative/path/of/root\n\
+      \n\
+      \032  is used to specify the Unix domain socket the client Unison should use\n\
+      \032  to contact the server.\n\
       \n\
       \032  The syntax for roots is based on that of URIs (described in RFC 2396).\n\
       \032  The full grammar is:\n\
@@ -749,11 +773,14 @@ let docs =
       \032 protocol ::= file\n\
       \032           |  socket\n\
       \032           |  ssh\n\
-      \032           |  rsh\n\
       \n\
       \032 user ::= [-_a-zA-Z0-9]+\n\
       \n\
       \032 host ::= [-_a-zA-Z0-9.]+\n\
+      \032       |  \\[ [a-f0-9:.]+ zone? \\]    IPv6 literals (no future format).\n\
+      \032       |  { [^}]+ }                   For Unix domain sockets only.\n\
+      \n\
+      \032 zone ::= %[-_a-zA-Z0-9~%.]+\n\
       \n\
       \032 port ::= [0-9]+\n\
       \n\
@@ -794,7 +821,7 @@ let docs =
       \032  /home/bcpierce/current/todo.txt on the server.)\n\
       \n\
       \032  The empty path (i.e., the empty sequence of names) denotes the whole\n\
-      \032  replica. Unison displays the empty path as \"[root].\"\n\
+      \032  replica. Unison displays the empty path as \226\128\156[root].\226\128\157\n\
       \n\
       \032  If p is a path and q is a path beginning with p, then q is said to be a\n\
       \032  descendant of p. (Each path is also a descendant of itself.)\n\
@@ -810,10 +837,10 @@ let docs =
       \032    * If p refers to a symbolic link, then the contents of p are just the\n\
       \032      string specifying where the link points.\n\
       \032    * If p refers to a directory, then the contents of p are just the\n\
-      \032      token \"DIRECTORY\" plus the current permission bits of the\n\
+      \032      token \226\128\156DIRECTORY\226\128\157 plus the current permission bits of the\n\
       \032      directory.\n\
       \032    * If p does not refer to anything in this replica, then the contents\n\
-      \032      of p are the token \"ABSENT.\"\n\
+      \032      of p are the token \226\128\156ABSENT.\226\128\157\n\
       \n\
       \032  Unison keeps a record of the contents of each path after each\n\
       \032  successful synchronization of that path (i.e., it remembers the\n\
@@ -823,15 +850,15 @@ let docs =
       \032  We say that a path is updated (in some replica) if its current contents\n\
       \032  are different from its contents the last time it was successfully\n\
       \032  synchronized. Note that whether a path is updated has nothing to do\n\
-      \032  with its last modification time--Unison considers only the contents\n\
-      \032  when determining whether an update has occurred. This means that\n\
-      \032  touching a file without changing its contents will not be recognized as\n\
-      \032  an update. A file can even be changed several times and then changed\n\
-      \032  back to its original contents; as long as Unison is only run at the end\n\
-      \032  of this process, no update will be recognized.\n\
+      \032  with its last modification time\226\128\148Unison considers only the contents when\n\
+      \032  determining whether an update has occurred. This means that touching a\n\
+      \032  file without changing its contents will not be recognized as an update.\n\
+      \032  A file can even be changed several times and then changed back to its\n\
+      \032  original contents; as long as Unison is only run at the end of this\n\
+      \032  process, no update will be recognized.\n\
       \n\
       \032  What Unison actually calculates is a close approximation to this\n\
-      \032  definition; see the section \"Caveats and Shortcomings\" .\n\
+      \032  definition; see the section \226\128\156Caveats and Shortcomings\226\128\157 .\n\
       \n\
       What is a Conflict?\n\
       \n\
@@ -848,7 +875,7 @@ let docs =
       \032      of each path in the replica when it was last synchronized) with the\n\
       \032      current contents of the replica, to determine which paths have been\n\
       \032      updated.\n\
-      \032   2. It checks for \"false conflicts\" -- paths that have been updated on\n\
+      \032   2. It checks for \226\128\156false conflicts\226\128\157 \226\128\148 paths that have been updated on\n\
       \032      both replicas, but whose current values are identical. These paths\n\
       \032      are silently marked as synchronized in the archive files in both\n\
       \032      replicas.\n\
@@ -882,7 +909,7 @@ let docs =
       \032      original contents (i.e., no change at all has been made to this\n\
       \032      path), or (2) its correct final contents (i.e., the value that the\n\
       \032      user expected to be propagated from the other replica).\n\
-      \032    * At every moment, the information stored on disk about Unison's\n\
+      \032    * At every moment, the information stored on disk about Unison\226\128\153s\n\
       \032      private state can be either (1) unchanged, or (2) updated to\n\
       \032      reflect those paths that have been successfully synchronized.\n\
       \n\
@@ -900,7 +927,7 @@ let docs =
       \n\
       \032  If an interruption happens while it is propagating updates, then there\n\
       \032  may be some paths for which an update has been propagated but which\n\
-      \032  have not been marked as synchronized in Unison's archives. This is no\n\
+      \032  have not been marked as synchronized in Unison\226\128\153s archives. This is no\n\
       \032  problem: the next time Unison runs, it will detect changes to these\n\
       \032  paths in both replicas, notice that the contents are now equal, and\n\
       \032  mark the paths as successfully updated when it writes back its private\n\
@@ -919,7 +946,7 @@ let docs =
       \032  them is that the clock on each system always runs forward.\n\
       \n\
       \032  If Unison finds that its archive files have been deleted (or that the\n\
-      \032  archive format has changed and they cannot be read, or that they don't\n\
+      \032  archive format has changed and they cannot be read, or that they don\226\128\153t\n\
       \032  exist because this is the first run of Unison on these particular\n\
       \032  roots), it takes a conservative approach: it behaves as though the\n\
       \032  replicas had both been completely empty at the point of the last\n\
@@ -930,15 +957,15 @@ let docs =
       \n\
       \032  Touching a file without changing its contents should never affect\n\
       \032  whether or not Unison does an update. (When running with the fastcheck\n\
-      \032  preference set to true--the default on Unix systems--Unison uses file\n\
+      \032  preference set to true\226\128\148the default on Unix systems\226\128\148Unison uses file\n\
       \032  modtimes for a quick first pass to tell which files have definitely not\n\
       \032  changed; then, for each file that might have changed, it computes a\n\
-      \032  fingerprint of the file's contents and compares it against the\n\
+      \032  fingerprint of the file\226\128\153s contents and compares it against the\n\
       \032  last-synchronized contents. Also, the -times option allows you to\n\
       \032  synchronize file times, but it does not cause identical files to be\n\
       \032  changed; Unison will only modify the file times.)\n\
       \n\
-      \032  It is safe to \"brainwash\" Unison by deleting its archive files on both\n\
+      \032  It is safe to \226\128\156brainwash\226\128\157 Unison by deleting its archive files on both\n\
       \032  replicas. The next time it runs, it will assume that all the files it\n\
       \032  sees in the replicas are new.\n\
       \n\
@@ -948,7 +975,7 @@ let docs =
       \032  for that file. Run Unison again to propagate the latest change.\n\
       \n\
       \032  Changes to the ignore patterns from the user interface (e.g., using the\n\
-      \032  `i' key) are immediately reflected in the current profile.\n\
+      \032  \226\128\152i\226\128\153 key) are immediately reflected in the current profile.\n\
       \n\
       Caveats and Shortcomings\n\
       \n\
@@ -956,15 +983,15 @@ let docs =
       \032    * In the interests of speed, the update detection algorithm may\n\
       \032      (depending on which OS architecture that you run Unison on)\n\
       \032      actually use an approximation to the definition given in the\n\
-      \032      section \"What is an Update?\" .\n\
+      \032      section \226\128\156What is an Update?\226\128\157 .\n\
       \032      In particular, the Unix implementation does not compare the actual\n\
       \032      contents of files to their previous contents, but simply looks at\n\
-      \032      each file's inode number and modtime; if neither of these have\n\
+      \032      each file\226\128\153s inode number and modtime; if neither of these have\n\
       \032      changed, then it concludes that the file has not been changed.\n\
       \032      Under normal circumstances, this approximation is safe, in the\n\
-      \032      sense that it may sometimes detect \"false updates\" but will never\n\
+      \032      sense that it may sometimes detect \226\128\156false updates\226\128\157 but will never\n\
       \032      miss a real one. However, it is possible to fool it, for example by\n\
-      \032      using retouch to change a file's modtime back to a time in the\n\
+      \032      using retouch to change a file\226\128\153s modtime back to a time in the\n\
       \032      past.\n\
       \032    * If you synchronize between a single-user filesystem and a shared\n\
       \032      Unix server, you should pay attention to your permission bits: by\n\
@@ -972,7 +999,7 @@ let docs =
       \032      leave group-writable files on the server that could be written over\n\
       \032      by a lot of people.\n\
       \032      You can control this by setting your umask on both computers to\n\
-      \032      something like 022, masking out the \"world write\" and \"group write\"\n\
+      \032      something like 022, masking out the \226\128\156world write\226\128\157 and \226\128\156group write\226\128\157\n\
       \032      permission bits.\n\
       \032      Unison does not synchronize the setuid and setgid bits, for\n\
       \032      security.\n\
@@ -985,12 +1012,12 @@ let docs =
       \032    * It is important to be a little careful when renaming directories\n\
       \032      containing ignored files.\n\
       \032      For example, suppose Unison is synchronizing directory A between\n\
-      \032      the two machines called the \"local\" and the \"remote\" machine;\n\
+      \032      the two machines called the \226\128\156local\226\128\157 and the \226\128\156remote\226\128\157 machine;\n\
       \032      suppose directory A contains a subdirectory D; and suppose D on the\n\
       \032      local machine contains a file or subdirectory P that matches an\n\
       \032      ignore directive in the profile used to synchronize. Thus path\n\
       \032      A/D/P exists on the local machine but not on the remote machine.\n\
-      \032      If D is renamed to D' on the remote machine, and this change is\n\
+      \032      If D is renamed to D\226\128\153 on the remote machine, and this change is\n\
       \032      propagated to the local machine, all such files or subdirectories P\n\
       \032      will be deleted. This is because Unison sees the rename as a delete\n\
       \032      and a separate create: it deletes the old directory (including the\n\
@@ -1010,16 +1037,16 @@ let docs =
      "Running Unison\n\
       \n\
       \032  There are several ways to start Unison.\n\
-      \032    * Typing \"unison profile\" on the command line. Unison will look for a\n\
+      \032    * Typing \226\128\156unison profile\226\128\157 on the command line. Unison will look for a\n\
       \032      file profile.prf in the .unison directory. If this file does not\n\
       \032      specify a pair of roots, Unison will prompt for them and add them\n\
       \032      to the information specified by the profile.\n\
-      \032    * Typing \"unison profile root1 root2\" on the command line. In this\n\
+      \032    * Typing \226\128\156unison profile root1 root2\226\128\157 on the command line. In this\n\
       \032      case, Unison will use profile, which should not contain any root\n\
       \032      directives.\n\
-      \032    * Typing \"unison root1 root2\" on the command line. This has the same\n\
-      \032      effect as typing \"unison default root1 root2.\"\n\
-      \032    * Typing just \"unison\" (or invoking Unison by clicking on a desktop\n\
+      \032    * Typing \226\128\156unison root1 root2\226\128\157 on the command line. This has the same\n\
+      \032      effect as typing \226\128\156unison default root1 root2.\226\128\157\n\
+      \032    * Typing just \226\128\156unison\226\128\157 (or invoking Unison by clicking on a desktop\n\
       \032      icon). In this case, Unison will ask for the profile to use for\n\
       \032      synchronization (or create a new one, if necessary).\n\
       \n\
@@ -1030,7 +1057,7 @@ let docs =
       \032  will be used as the path/folder name for this directory. This can be\n\
       \032  just a name, or a path.\n\
       \n\
-      \032  A name on it's own, for example UNISON=mytestname will place a folder\n\
+      \032  A name on it\226\128\153s own, for example UNISON=mytestname will place a folder\n\
       \032  in the same directory that the Unison binary was run in, with that\n\
       \032  name. Using a path like UNISON=../mytestname2 will place that folder in\n\
       \032  the folder above where the Unison binary was run from.\n\
@@ -1045,7 +1072,7 @@ let docs =
       \032  default.\n\
       \n\
       \032  The archive file for each replica is found in the .unison directory on\n\
-      \032  that replica's host. Profiles (described below) are always taken from\n\
+      \032  that replica\226\128\153s host. Profiles (described below) are always taken from\n\
       \032  the .unison directory on the client host.\n\
       \n\
       \032  Note that Unison maintains a completely different set of archive files\n\
@@ -1077,7 +1104,7 @@ let docs =
       \032  to remember which files have been synchronized) normally uses the\n\
       \032  gethostname operating system call. However, if the environment variable\n\
       \032  UNISONLOCALHOSTNAME is set, its value will be used instead. This makes\n\
-      \032  it easier to use Unison in situations where a machine's name changes\n\
+      \032  it easier to use Unison in situations where a machine\226\128\153s name changes\n\
       \032  frequently (e.g., because it is a laptop and gets moved around a lot).\n\
       \n\
       \032  A more powerful way of changing archive names is provided by the\n\
@@ -1097,21 +1124,21 @@ let docs =
       \032  file systems.\n\
       \n\
       \032  Warning: The rootalias option is dangerous and should only be used if\n\
-      \032  you are sure you know what you're doing. In particular, it should only\n\
+      \032  you are sure you know what you\226\128\153re doing. In particular, it should only\n\
       \032  be used if you are positive that either (1) both the original root and\n\
       \032  the new alias refer to the same set of files, or (2) the files have\n\
       \032  been relocated so that the original name is now invalid and will never\n\
       \032  be used again. (If the original root and the alias refer to different\n\
-      \032  sets of files, Unison's update detector could get confused.) After\n\
+      \032  sets of files, Unison\226\128\153s update detector could get confused.) After\n\
       \032  introducing a new rootalias, it is a good idea to run Unison a few\n\
       \032  times interactively (with the batch flag off, etc.) and carefully check\n\
-      \032  that things look reasonable--in particular, that update detection is\n\
+      \032  that things look reasonable\226\128\148in particular, that update detection is\n\
       \032  working as expected.\n\
       \n\
       Preferences\n\
       \n\
-      \032  Many details of Unison's behavior are configurable by user-settable\n\
-      \032  \"preferences.\"\n\
+      \032  Many details of Unison\226\128\153s behavior are configurable by user-settable\n\
+      \032  \226\128\156preferences.\226\128\157\n\
       \n\
       \032  Some preferences are boolean-valued; these are often called flags.\n\
       \032  Others take numeric or string arguments, indicated in the preferences\n\
@@ -1143,131 +1170,167 @@ let docs =
       \032   or unison profilename [options]\n\
       \n\
       Basic options:\n\
-      \032-auto              automatically accept default (nonconflicting) actions\n\
-      \032-batch             batch mode: ask no questions at all\n\
-      \032-doc xxx           show documentation ('-doc topics' lists topics)\n\
-      \032-fat               use appropriate options for FAT filesystems\n\
-      \032-group             synchronize group attributes\n\
-      \032-i                 interactive profile mode (text UI); command-line only\n\
-      \032-ignore xxx        add a pattern to the ignore list\n\
-      \032-ignorenot xxx     add a pattern to the ignorenot list\n\
-      \032-nocreation xxx    prevent file creations on one replica\n\
-      \032-nodeletion xxx    prevent file deletions on one replica\n\
-      \032-noupdate xxx      prevent file updates and deletions on one replica\n\
-      \032-owner             synchronize owner\n\
-      \032-path xxx          path to synchronize\n\
-      \032-perms n           part of the permissions which is synchronized\n\
-      \032-root xxx          root of a replica (should be used exactly twice)\n\
-      \032-silent            print nothing except error messages\n\
-      \032-terse             suppress status messages\n\
-      \032-testserver        exit immediately after the connection to the server\n\
-      \032-times             synchronize modification times\n\
-      \032-version           print version and exit\n\
+      \n\
+      \032 General:\n\
+      \032  -doc xxx            show documentation ('-doc topics' lists topics)\n\
+      \032  -version            print version and exit\n\
+      \n\
+      \032 What to sync:\n\
+      \032  -group              synchronize group attributes\n\
+      \032  -ignore xxx         add a pattern to the ignore list\n\
+      \032  -ignorenot xxx      add a pattern to the ignorenot list\n\
+      \032  -nocreation xxx     prevent file creations on one replica\n\
+      \032  -nodeletion xxx     prevent file deletions on one replica\n\
+      \032  -noupdate xxx       prevent file updates and deletions on one replica\n\
+      \032  -owner              synchronize owner\n\
+      \032  -path xxx           path to synchronize\n\
+      \032  -perms n            part of the permissions which is synchronized\n\
+      \032  -root xxx           root of a replica (should be used exactly twice)\n\
+      \032  -times              synchronize modification times\n\
+      \n\
+      \032 How to sync:\n\
+      \032  -batch              batch mode: ask no questions at all\n\
+      \n\
+      \032 How to sync (text interface (CLI) only):\n\
+      \032  -auto               automatically accept default (nonconflicting) actions\n\
+      \032  -silent             print nothing except error messages\n\
+      \032  -terse              suppress status messages\n\
+      \n\
+      \032 Text interface (CLI):\n\
+      \032  -i                  interactive profile mode (text UI); command-line only\n\
       \n\
       Advanced options:\n\
-      \032-addprefsto xxx    file to add new prefs to\n\
-      \032-addversionno      add version number to name of unison on server\n\
-      \032-atomic xxx        add a pattern to the atomic list\n\
-      \032-backup xxx        add a pattern to the backup list\n\
-      \032-backupcurr xxx    add a pattern to the backupcurr list\n\
-      \032-backupcurrnot xxx add a pattern to the backupcurrnot list\n\
-      \032-backupdir xxx     directory for storing centralized backups\n\
-      \032-backuploc xxx     where backups are stored ('local' or 'central')\n\
-      \032-backupnot xxx     add a pattern to the backupnot list\n\
-      \032-backupprefix xxx  prefix for the names of backup files\n\
-      \032-backups           keep backup copies of all files (see also 'backup')\n\
-      \032-backupsuffix xxx  a suffix to be added to names of backup files\n\
-      \032-clientHostName xxx set host name of client\n\
-      \032-color xxx         use color output for text UI (true/false/default)\n\
-      \032-confirmbigdel     ask about whole-replica (or path) deletes (default true)\n\
-      \032-confirmmerge      ask for confirmation before committing results of a merge\n\
-      \032-contactquietly    suppress the 'contacting server' message during startup\n\
-      \032-copymax n         maximum number of simultaneous copyprog transfers\n\
-      \032-copyonconflict    keep copies of conflicting files\n\
-      \032-copyprog xxx      external program for copying large files\n\
-      \032-copyprogrest xxx  variant of copyprog for resuming partial transfers\n\
-      \032-copyquoterem xxx  add quotes to remote file name for copyprog (true/false/defa\n\
-      ult)\n\
-      \032-copythreshold n   use copyprog on files bigger than this (if >=0, in Kb)\n\
-      \032-debug xxx         debug module xxx ('all' -> everything, 'verbose' -> more)\n\
-      \032-diff xxx          set command for showing differences between files\n\
-      \032-dontchmod         when set, never use the chmod system call\n\
-      \032-dumbtty           do not change terminal settings in text UI\n\
-      \032-fastcheck xxx     do fast update detection (true/false/default)\n\
-      \032-fastercheckUNSAFE skip computing fingerprints for new files (experts only!)\n\
-      \032-follow xxx        add a pattern to the follow list\n\
-      \032-force xxx         force changes from this replica to the other\n\
-      \032-forcepartial xxx  add a pattern to the forcepartial list\n\
-      \032-halfduplex        force half-duplex communication with the server\n\
-      \032-height n          height (in lines) of main window in graphical interface\n\
-      \032-host xxx          bind the socket to this host name in server socket mode\n\
-      \032-ignorearchives    ignore existing archive files\n\
-      \032-ignorecase xxx    identify upper/lowercase filenames (true/false/default)\n\
-      \032-ignoreinodenumbers ignore inode number changes when detecting updates\n\
-      \032-ignorelocks       ignore locks left over from previous run (dangerous!)\n\
-      \032-immutable xxx     add a pattern to the immutable list\n\
-      \032-immutablenot xxx  add a pattern to the immutablenot list\n\
-      \032-key xxx           define a keyboard shortcut for this profile (in some UIs)\n\
-      \032-killserver        kill server when done (even when using sockets)\n\
-      \032-label xxx         provide a descriptive string label for this profile\n\
-      \032-links xxx         allow the synchronization of symbolic links (true/false/defa\n\
-      ult)\n\
-      \032-log               record actions in logfile (default true)\n\
-      \032-logfile xxx       logfile name\n\
-      \032-maxbackups n      number of backed up versions of a file\n\
-      \032-maxerrors n       maximum number of errors before a directory transfer is abor\n\
-      ted\n\
-      \032-maxsizethreshold n prevent transfer of files bigger than this (if >=0, in Kb)\n\
-      \032-maxthreads n      maximum number of simultaneous file transfers\n\
-      \032-merge xxx         add a pattern to the merge list\n\
-      \032-mountpoint xxx    abort if this path does not exist\n\
-      \032-nocreationpartial xxx add a pattern to the nocreationpartial list\n\
-      \032-nodeletionpartial xxx add a pattern to the nodeletionpartial list\n\
-      \032-noupdatepartial xxx add a pattern to the noupdatepartial list\n\
-      \032-numericids        don't map uid/gid values by user/group names\n\
-      \032-prefer xxx        choose this replica's version for conflicting changes\n\
-      \032-preferpartial xxx add a pattern to the preferpartial list\n\
-      \032-repeat xxx        synchronize repeatedly (text interface only)\n\
-      \032-retry n           re-try failed synchronizations N times (text ui only)\n\
-      \032-rootalias xxx     register alias for canonical root names\n\
-      \032-rsrc xxx          synchronize resource forks (true/false/default)\n\
-      \032-rsync             activate the rsync transfer mode (default true)\n\
-      \032-selftest          run internal tests and exit\n\
-      \032-servercmd xxx     name of unison executable on remote server\n\
-      \032-showarchive       show 'true names' (for rootalias) of roots and archive\n\
-      \032-socket xxx        act as a server on a socket\n\
-      \032-sortbysize        list changed files by size, not name\n\
-      \032-sortfirst xxx     add a pattern to the sortfirst list\n\
-      \032-sortlast xxx      add a pattern to the sortlast list\n\
-      \032-sortnewfirst      list new before changed files\n\
-      \032-sshargs xxx       other arguments (if any) for remote shell command\n\
-      \032-sshcmd xxx        path to the ssh executable\n\
-      \032-stream            use a streaming protocol for transferring file contents (def\n\
-      ault true)\n\
-      \032-ui xxx            select UI ('text' or 'graphic'); command-line only\n\
-      \032-unicode xxx       assume Unicode encoding in case insensitive mode\n\
-      \032-watch             when set, use a file watcher process to detect changes\n\
-      \032-xferbycopying     optimize transfers using local copies (default true)\n\
-      \n\
-      Special command line options:\n\
-      \032-include xxx       include a profile file's preferences\n\
-      \032-source xxx        include a file's preferences\n\
+      \n\
+      \032 Fine-tune sync:\n\
+      \032  -acl                synchronize ACLs\n\
+      \032  -atomic xxx         add a pattern to the atomic list\n\
+      \032  -follow xxx         add a pattern to the follow list\n\
+      \032  -force xxx          force changes from this replica to the other\n\
+      \032  -forcepartial xxx   add a pattern to the forcepartial list\n\
+      \032  -ignorecase xxx     identify upper/lowercase filenames (true/false/default)\n\
+      \032  -immutable xxx      add a pattern to the immutable list\n\
+      \032  -immutablenot xxx   add a pattern to the immutablenot list\n\
+      \032  -links xxx          allow the synchronization of symbolic links\n\
+      \032                      (true/false/default)\n\
+      \032  -merge xxx          add a pattern to the merge list\n\
+      \032  -nocreationpartial xxx add a pattern to the nocreationpartial list\n\
+      \032  -nodeletionpartial xxx add a pattern to the nodeletionpartial list\n\
+      \032  -noupdatepartial xxx add a pattern to the noupdatepartial list\n\
+      \032  -prefer xxx         choose this replica's version for conflicting changes\n\
+      \032  -preferpartial xxx  add a pattern to the preferpartial list\n\
+      \032  -rsrc xxx           synchronize resource forks (true/false/default)\n\
+      \032  -xattrignore xxx    add a pattern to the xattrignore list\n\
+      \032  -xattrignorenot xxx add a pattern to the xattrignorenot list\n\
+      \032  -xattrs             synchronize extended attributes (xattrs)\n\
+      \n\
+      \032 How to sync:\n\
+      \032  -backup xxx         add a pattern to the backup list\n\
+      \032  -backupcurr xxx     add a pattern to the backupcurr list\n\
+      \032  -backupcurrnot xxx  add a pattern to the backupcurrnot list\n\
+      \032  -backupdir xxx      directory for storing centralized backups\n\
+      \032  -backuploc xxx      where backups are stored ('local' or 'central')\n\
+      \032  -backupnot xxx      add a pattern to the backupnot list\n\
+      \032  -backupprefix xxx   prefix for the names of backup files\n\
+      \032  -backups            (deprecated) keep backup copies of all files (see also\n\
+      \032                      'backup')\n\
+      \032  -backupsuffix xxx   a suffix to be added to names of backup files\n\
+      \032  -confirmbigdel      ask about whole-replica (or path) deletes (default true)\n\
+      \032  -confirmmerge       ask for confirmation before committing results of a merge\n\
+      \032  -copyonconflict     keep copies of conflicting files\n\
+      \032  -dontchmod          when set, never use the chmod system call\n\
+      \032  -fastcheck xxx      do fast update detection (true/false/default)\n\
+      \032  -fat                use appropriate options for FAT filesystems\n\
+      \032  -ignoreinodenumbers ignore inode number changes when detecting updates\n\
+      \032  -maxbackups n       number of backed up versions of a file\n\
+      \032  -numericids         don't map uid/gid values by user/group names\n\
+      \032  -sortbysize         list changed files by size, not name\n\
+      \032  -sortfirst xxx      add a pattern to the sortfirst list\n\
+      \032  -sortlast xxx       add a pattern to the sortlast list\n\
+      \032  -sortnewfirst       list new before changed files\n\
+      \n\
+      \032 How to sync (text interface (CLI) only):\n\
+      \032  -repeat xxx         synchronize repeatedly (text interface only)\n\
+      \032  -retry n            re-try failed synchronizations N times (text ui only)\n\
+      \n\
+      \032 Text interface (CLI):\n\
+      \032  -color xxx          use color output for text UI (true/false/default)\n\
+      \032  -dumbtty            do not change terminal settings in text UI\n\
+      \n\
+      \032 Graphical interface (GUI):\n\
+      \032  -height n           height (in lines) of main window in graphical interface\n\
+      \n\
+      \032 Remote connections:\n\
+      \032  -addversionno       add version number to name of unison on server\n\
+      \032  -clientHostName xxx set host name of client\n\
+      \032  -halfduplex         (deprecated) force half-duplex communication with the\n\
+      \032                      server\n\
+      \032  -killserver         kill server when done (even when using sockets)\n\
+      \032  -listen xxx         listen on this name or addr in server socket mode (can\n\
+      \032                      repeat)\n\
+      \032  -rsync              activate the rsync transfer mode (default true)\n\
+      \032  -servercmd xxx      name of unison executable on remote server\n\
+      \032  -socket xxx         act as a server on a socket\n\
+      \032  -sshargs xxx        other arguments (if any) for remote shell command\n\
+      \032  -sshcmd xxx         path to the ssh executable\n\
+      \032  -stream             (deprecated) use a streaming protocol for transferring\n\
+      \032                      file contents (default true)\n\
+      \032  -testserver         exit immediately after the connection to the server\n\
+      \032  -xferbycopying      optimize transfers using local copies (default true)\n\
+      \n\
+      \032 Archive management:\n\
+      \032  -ignorearchives     ignore existing archive files\n\
+      \n\
+      \032 Other:\n\
+      \032  -addprefsto xxx     file to add new prefs to\n\
+      \032  -contactquietly     suppress the 'contacting server' message during startup\n\
+      \032  -copymax n          maximum number of simultaneous copyprog transfers\n\
+      \032  -copyprog xxx       external program for copying large files\n\
+      \032  -copyprogrest xxx   variant of copyprog for resuming partial transfers\n\
+      \032  -copythreshold n    use copyprog on files bigger than this (if >=0, in Kb)\n\
+      \032  -diff xxx           set command for showing differences between files\n\
+      \032  -ignorelocks        ignore locks left over from previous run (dangerous!)\n\
+      \032  -include xxx        include a profile's preferences\n\
+      \032  -key xxx            define a keyboard shortcut for this profile (in some UIs)\n\
+      \032  -label xxx          provide a descriptive string label for this profile\n\
+      \032  -log                record actions in logfile (default true)\n\
+      \032  -logfile xxx        logfile name\n\
+      \032  -maxerrors n        maximum number of errors before a directory transfer is\n\
+      \032                      aborted\n\
+      \032  -maxsizethreshold n prevent transfer of files bigger than this (if >=0, in\n\
+      \032                      Kb)\n\
+      \032  -maxthreads n       maximum number of simultaneous file transfers\n\
+      \032  -mountpoint xxx     abort if this path does not exist\n\
+      \032  -rootalias xxx      register alias for canonical root names\n\
+      \032  -showarchive        show 'true names' (for rootalias) of roots and archive\n\
+      \032  -source xxx         include a file's preferences\n\
+      \032  -ui xxx             select UI ('text' or 'graphic'); command-line only\n\
+      \032  -unicode xxx        assume Unicode encoding in case insensitive mode\n\
+      \032  -watch              when set, use a file watcher process to detect changes\n\
+      \n\
+      Expert options:\n\
+      \032  -debug xxx          debug module xxx ('all' -> everything, 'verbose' -> more)\n\
+      \032  -dumparchives       dump contents of archives just after loading\n\
+      \032  -fastercheckUNSAFE  skip computing fingerprints for new files (experts only!)\n\
+      \032  -selftest           run internal tests and exit\n\
       \n\
       \n\
       \032  Here, in more detail, is what they do. Many are discussed in greater\n\
       \032  detail in other sections of the manual.\n\
       \n\
       \032  It should be noted that some command-line arguments are handled\n\
-      \032  specially during startup, including -doc, -help, -version, -server,\n\
-      \032  -socket, and -ui. They are expected to appear on the command-line only,\n\
-      \032  not in a profile. In particular, -version and -doc will print to the\n\
-      \032  standard output, so they only make sense if invoked from the\n\
-      \032  command-line (and not a click-launched gui that has no standard\n\
-      \032  output). Furthermore, the actions associated with these command-line\n\
-      \032  arguments are executed without loading a profile or doing the usual\n\
-      \032  command-line parsing. This is because we want to run the actions\n\
-      \032  without loading a profile; and then we can't do command-line parsing\n\
-      \032  because it is intertwined with profile loading.\n\
+      \032  specially during startup, including -doc, -help, -version, -socket, and\n\
+      \032  -ui. They are expected to appear on the command-line only, not in a\n\
+      \032  profile. In particular, -version and -doc will print to the standard\n\
+      \032  output, so they only make sense if invoked from the command-line (and\n\
+      \032  not a click-launched gui that has no standard output). Furthermore, the\n\
+      \032  actions associated with these command-line arguments are executed\n\
+      \032  without loading a profile or doing the usual command-line parsing.\n\
+      \n\
+      \032  acl\n\
+      \032         When this flag is set to true, the ACLs of files and directories\n\
+      \032         are synchronized. The type of ACLs depends on the platform and\n\
+      \032         filesystem support. On Unix-like platforms it can be NFSv4 ACLs,\n\
+      \032         for example.\n\
       \n\
       \032  addprefsto xxx\n\
       \032         By default, new preferences added by Unison (e.g., new ignore\n\
@@ -1280,7 +1343,7 @@ let docs =
       \032         When this flag is set to true, Unison will use\n\
       \032         unison-currentmajorversionnumber instead of just unison as the\n\
       \032         remote server command (note that the minor version number is\n\
-      \032         dropped - e.g., unison-2.51). This allows multiple binaries for\n\
+      \032         dropped \226\128\147 e.g., unison-2.51). This allows multiple binaries for\n\
       \032         different versions of unison to coexist conveniently on the same\n\
       \032         server: whichever version is run on the client, the same version\n\
       \032         will be selected on the server.\n\
@@ -1310,20 +1373,20 @@ let docs =
       \032         versions that are kept is determined by the maxbackups\n\
       \032         preference.\n\
       \n\
-      \032         The syntax of pathspec is described in the section \"Path\n\
-      \032         Specification\" .\n\
+      \032         The syntax of pathspec is described in the section \226\128\156Path\n\
+      \032         Specification\226\128\157 .\n\
       \n\
       \032  backupcurr xxx\n\
       \032         Including the preference -backupcurr pathspec causes Unison to\n\
       \032         keep a backup of the current version of every file matching\n\
       \032         pathspec. This file will be saved as a backup with version\n\
       \032         number 000. Such backups can be used as inputs to external\n\
-      \032         merging programs, for instance. See the documentatation for the\n\
-      \032         merge preference. For more details, see the section \"Merging\n\
-      \032         Conflicting Versions\" .\n\
+      \032         merging programs, for instance. See the documentation for the\n\
+      \032         merge preference. For more details, see the section \226\128\156Merging\n\
+      \032         Conflicting Versions\226\128\157 .\n\
       \n\
-      \032         The syntax of pathspec is described in the section \"Path\n\
-      \032         Specification\" .\n\
+      \032         The syntax of pathspec is described in the section \226\128\156Path\n\
+      \032         Specification\226\128\157 .\n\
       \n\
       \032  backupcurrnot xxx\n\
       \032         Exceptions to backupcurr, like the ignorenot preference.\n\
@@ -1344,7 +1407,7 @@ let docs =
       \032  backupnot xxx\n\
       \032         The values of this preference specify paths or individual files\n\
       \032         or regular expressions that should not be backed up, even if the\n\
-      \032         backup preference selects them--i.e., it selectively overrides\n\
+      \032         backup preference selects them\226\128\148i.e., it selectively overrides\n\
       \032         backup.\n\
       \n\
       \032  backupprefix xxx\n\
@@ -1353,7 +1416,7 @@ let docs =
       \032         backupprefixNAMEbackupsuffix. backupprefix can include a\n\
       \032         directory name (causing Unison to keep all backup files for a\n\
       \032         given directory in a subdirectory with this name), and both\n\
-      \032         backupprefix and backupsuffix can contain the string$VERSION,\n\
+      \032         backupprefix and backupsuffix can contain the string $VERSION,\n\
       \032         which will be replaced by the age of the backup (1 for the most\n\
       \032         recent, 2 for the second most recent, and so on...). This\n\
       \032         keyword is ignored if it appears in a directory name in the\n\
@@ -1368,7 +1431,7 @@ let docs =
       \032         sufficiently different from the names of your real files.\n\
       \n\
       \032  backups\n\
-      \032         Setting this flag to true is equivalent to setting\n\
+      \032         (Deprecated) Setting this flag to true is equivalent to setting\n\
       \032         backuplocation to local and backup to Name *.\n\
       \n\
       \032  backupsuffix xxx\n\
@@ -1380,9 +1443,8 @@ let docs =
       \032         conflicts will be skipped.\n\
       \n\
       \032  clientHostName xxx\n\
-      \032         When specified, the host name of the client will not be\n\
-      \032         guessedand the provided host name will be used to find the\n\
-      \032         archive.\n\
+      \032         When specified, the host name of the client will not be guessed\n\
+      \032         and the provided host name will be used to find the archive.\n\
       \n\
       \032  color xxx\n\
       \032         When set to true, this flag enables color output in text mode\n\
@@ -1409,8 +1471,8 @@ let docs =
       \032         Default is false.\n\
       \n\
       \032  contactquietly\n\
-      \032         If this flag is set, Unison will skip displaying the `Contacting\n\
-      \032         server' message (which some users find annoying) during startup.\n\
+      \032         If this flag is set, Unison will skip displaying the \226\128\152Contacting\n\
+      \032         server\226\128\153 message (which some users find annoying) during startup.\n\
       \n\
       \032  copymax n\n\
       \032         A number indicating how many instances of the external copying\n\
@@ -1429,7 +1491,7 @@ let docs =
       \032         A string giving the name of an external program that can be used\n\
       \032         to copy large files efficiently (plus command-line switches\n\
       \032         telling it to copy files in-place). The default setting invokes\n\
-      \032         rsync with appropriate options--most users should not need to\n\
+      \032         rsync with appropriate options\226\128\148most users should not need to\n\
       \032         change it.\n\
       \n\
       \032  copyprogrest xxx\n\
@@ -1438,24 +1500,15 @@ let docs =
       \032         already been partially transferred. Typically, copyprogrest will\n\
       \032         just be copyprog with one extra option (e.g., --partial, for\n\
       \032         rsync). The default setting invokes rsync with appropriate\n\
-      \032         options--most users should not need to change it.\n\
-      \n\
-      \032  copyquoterem xxx\n\
-      \032         When set to true, this flag causes Unison to add an extra layer\n\
-      \032         of quotes to the remote path passed to the external copy\n\
-      \032         program. This is needed by rsync, for example, which internally\n\
-      \032         uses an ssh connection requiring an extra level of quoting for\n\
-      \032         paths containing spaces. When this flag is set to default, extra\n\
-      \032         quotes are added if the value of copyprog contains the string\n\
-      \032         rsync.\n\
+      \032         options\226\128\148most users should not need to change it.\n\
       \n\
       \032  copythreshold n\n\
       \032         A number indicating above what filesize (in kilobytes) Unison\n\
       \032         should use the external copying utility specified by copyprog.\n\
       \032         Specifying 0 will cause all copies to use the external program;\n\
       \032         a negative number will prevent any files from using it. The\n\
-      \032         default is -1. See the section \"Making Unison Faster on Large\n\
-      \032         Files\" for more information.\n\
+      \032         default is -1. See the section \226\128\156Making Unison Faster on Large\n\
+      \032         Files\226\128\157 for more information.\n\
       \n\
       \032  debug xxx\n\
       \032         This preference is used to make Unison print various sorts of\n\
@@ -1474,7 +1527,7 @@ let docs =
       \032  diff xxx\n\
       \032         This preference can be used to control the name and command-line\n\
       \032         arguments of the system utility used to generate displays of\n\
-      \032         file differences. The default is `diff -u OLDER NEWER'. If the\n\
+      \032         file differences. The default is \226\128\152diff -u OLDER NEWER\226\128\153. If the\n\
       \032         value of this preference contains the substrings CURRENT1 and\n\
       \032         CURRENT2, these will be replaced by the names of the files to be\n\
       \032         diffed. If the value of this preference contains the substrings\n\
@@ -1493,7 +1546,7 @@ let docs =
       \032         of the various sections that can be printed.\n\
       \n\
       \032  dontchmod\n\
-      \032         By default, Unison uses the 'chmod' system call to set the\n\
+      \032         By default, Unison uses the \226\128\153chmod\226\128\153 system call to set the\n\
       \032         permission bits of files after it has copied them. But in some\n\
       \032         circumstances (and under some operating systems), the chmod call\n\
       \032         always fails. Setting this preference completely prevents Unison\n\
@@ -1502,7 +1555,7 @@ let docs =
       \032  dumbtty\n\
       \032         When set to true, this flag makes the text mode user interface\n\
       \032         avoid trying to change any of the terminal settings. (Normally,\n\
-      \032         Unison puts the terminal in `raw mode', so that it can do things\n\
+      \032         Unison puts the terminal in \226\128\152raw mode\226\128\153, so that it can do things\n\
       \032         like overwriting the current line.) This is useful, for example,\n\
       \032         when Unison runs in a shell inside of Emacs.\n\
       \n\
@@ -1520,8 +1573,8 @@ let docs =
       \n\
       \032  fastcheck xxx\n\
       \032         When this preference is set to true, Unison will use the\n\
-      \032         modification time and length of a file as a `pseudo inode\n\
-      \032         number' when scanning replicas for updates, instead of reading\n\
+      \032         modification time and length of a file as a \226\128\152pseudo inode\n\
+      \032         number\226\128\153 when scanning replicas for updates, instead of reading\n\
       \032         the full contents of every file. (This does not apply to the\n\
       \032         very first run, when Unison will always scan all files\n\
       \032         regardless of this switch). Under Windows, this may cause Unison\n\
@@ -1534,7 +1587,7 @@ let docs =
       \032         Unison once with fastcheck set to false, if you are worried that\n\
       \032         Unison may have overlooked an update. For backward\n\
       \032         compatibility, yes, no, and default can be used in place of\n\
-      \032         true, false, and auto. See the section \"Fast Update Detection\"\n\
+      \032         true, false, and auto. See the section \226\128\156Fast Update Detection\226\128\157\n\
       \032         for more information.\n\
       \n\
       \032  fastercheckUNSAFE\n\
@@ -1542,7 +1595,7 @@ let docs =
       \032         EXTREME CAUTION.\n\
       \n\
       \032         When this flag is set to true, Unison will compute a\n\
-      \032         'pseudo-fingerprint' the first time it sees a file (either\n\
+      \032         \226\128\153pseudo-fingerprint\226\128\153 the first time it sees a file (either\n\
       \032         because the file is new or because Unison is running for the\n\
       \032         first time). This enormously speeds update detection, but it\n\
       \032         must be used with care, as it can cause Unison to miss\n\
@@ -1560,13 +1613,13 @@ let docs =
       \032         has no record of the earlier contents) and show it as needing to\n\
       \032         be propagated to the other replica.\n\
       \n\
-      \032         Most users should leave this flag off - the small time savings\n\
+      \032         Most users should leave this flag off \226\128\147 the small time savings\n\
       \032         of not fingerprinting new files is not worth the cost in terms\n\
       \032         of safety. However, it can be very useful for power users with\n\
       \032         huge replicas that are known to be already synchronized (e.g.,\n\
       \032         because one replica is a newly created duplicate of the other,\n\
       \032         or because they have previously been synchronized with Unison\n\
-      \032         but Unison's archives need to be rebuilt). In such situations,\n\
+      \032         but Unison\226\128\153s archives need to be rebuilt). In such situations,\n\
       \032         it is recommended that this flag be set only for the initial run\n\
       \032         of Unison, so that new archives can be created quickly, and then\n\
       \032         turned off for normal use.\n\
@@ -1575,7 +1628,7 @@ let docs =
       \032         When this is set to true, Unison will use appropriate options to\n\
       \032         synchronize efficiently and without error a replica located on a\n\
       \032         FAT filesystem on a non-Windows machine: do not synchronize\n\
-      \032         permissions (perms = 0); never use chmod ( t dontchmod = true);\n\
+      \032         permissions (perms = 0); never use chmod (dontchmod = true);\n\
       \032         treat filenames as case insensitive (ignorecase = true); do not\n\
       \032         attempt to synchronize symbolic links (links = false); ignore\n\
       \032         inode number changes when detecting updates (ignoreinodenumbers\n\
@@ -1584,11 +1637,11 @@ let docs =
       \n\
       \032  follow xxx\n\
       \032         Including the preference -follow pathspec causes Unison to treat\n\
-      \032         symbolic links matching pathspec as `invisible' and behave as if\n\
+      \032         symbolic links matching pathspec as \226\128\152invisible\226\128\153 and behave as if\n\
       \032         the object pointed to by the link had appeared literally at this\n\
-      \032         position in the replica. See the section \"Symbolic Links\" for\n\
+      \032         position in the replica. See the section \226\128\156Symbolic Links\226\128\157 for\n\
       \032         more details. The syntax of pathspec is described in the section\n\
-      \032         \"Path Specification\" .\n\
+      \032         \226\128\156Path Specification\226\128\157 .\n\
       \n\
       \032  force xxx\n\
       \032         Including the preference -force root causes Unison to resolve\n\
@@ -1596,6 +1649,10 @@ let docs =
       \032         This effectively changes Unison from a synchronizer into a\n\
       \032         mirroring utility.\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         You can also specify -force newer (or -force older) to force\n\
       \032         Unison to choose the file with the later (earlier) modtime. In\n\
       \032         this case, the -times preference must also be enabled.\n\
@@ -1609,12 +1666,16 @@ let docs =
       \032         Including the preference forcepartial = PATHSPEC -> root causes\n\
       \032         Unison to resolve all differences (even non-conflicting changes)\n\
       \032         in favor of root for the files in PATHSPEC (see the section\n\
-      \032         \"Path Specification\" for more information). This effectively\n\
+      \032         \226\128\156Path Specification\226\128\157 for more information). This effectively\n\
       \032         changes Unison from a synchronizer into a mirroring utility.\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         You can also specify forcepartial PATHSPEC -> newer (or\n\
-      \032         forcepartial PATHSPEC older) to force Unison to choose the file\n\
-      \032         with the later (earlier) modtime. In this case, the -times\n\
+      \032         forcepartial PATHSPEC -> older) to force Unison to choose the\n\
+      \032         file with the later (earlier) modtime. In this case, the -times\n\
       \032         preference must also be enabled.\n\
       \n\
       \032         This preference should be used only if you are sure you know\n\
@@ -1626,13 +1687,10 @@ let docs =
       \032         identifiers are synchronized depends on the preference numerids.\n\
       \n\
       \032  halfduplex\n\
-      \032         When this flag is set to true, Unison network communication is\n\
-      \032         forced to be half duplex (the client and the server never\n\
-      \032         simultaneously emit data). If you experience unstabilities with\n\
-      \032         your network link, this may help. The communication is always\n\
-      \032         half-duplex when synchronizing with a Windows machine due to a\n\
-      \032         limitation of Unison current implementation that could result in\n\
-      \032         a deadlock.\n\
+      \032         (Deprecated) When this flag is set to true, Unison network\n\
+      \032         communication is forced to be half duplex (the client and the\n\
+      \032         server never simultaneously emit data). If you experience\n\
+      \032         unstabilities with your network link, this may help.\n\
       \n\
       \032  height n\n\
       \032         Used to set the height (in lines) of the main window in the\n\
@@ -1650,8 +1708,8 @@ let docs =
       \032         completely ignore paths that match pathspec (as well as their\n\
       \032         children). This is useful for avoiding synchronizing temporary\n\
       \032         files, object files, etc. The syntax of pathspec is described in\n\
-      \032         the section \"Path Specification\" , and further details on\n\
-      \032         ignoring paths is found in the section \"Ignoring Paths\" .\n\
+      \032         the section \226\128\156Path Specification\226\128\157 , and further details on\n\
+      \032         ignoring paths is found in the section \226\128\156Ignoring Paths\226\128\157 .\n\
       \n\
       \032  ignorearchives\n\
       \032         When this preference is set, Unison will ignore any existing\n\
@@ -1661,8 +1719,8 @@ let docs =
       \n\
       \032  ignorecase xxx\n\
       \032         When set to true, this flag causes Unison to treat filenames as\n\
-      \032         case insensitive--i.e., files in the two replicas whose names\n\
-      \032         differ in (upper- and lower-case) `spelling' are treated as the\n\
+      \032         case insensitive\226\128\148i.e., files in the two replicas whose names\n\
+      \032         differ in (upper- and lower-case) \226\128\152spelling\226\128\153 are treated as the\n\
       \032         same file. When the flag is set to false, Unison will treat all\n\
       \032         filenames as case sensitive. Ordinarily, when the flag is set to\n\
       \032         default, filenames are automatically taken to be\n\
@@ -1710,7 +1768,7 @@ let docs =
       \n\
       \032  immutable xxx\n\
       \032         This preference specifies paths for directories whose immediate\n\
-      \032         children are all immutable files -- i.e., once a file has been\n\
+      \032         children are all immutable files \226\128\148 i.e., once a file has been\n\
       \032         created, its contents never changes. When scanning for updates,\n\
       \032         Unison does not check whether these files have been modified;\n\
       \032         this can speed update detection significantly (in particular,\n\
@@ -1719,6 +1777,12 @@ let docs =
       \032  immutablenot xxx\n\
       \032         This preference overrides immutable.\n\
       \n\
+      \032  include xxx\n\
+      \032         Include preferences from a profile. include name reads the\n\
+      \032         profile \"name\" (or file \"name\" in the .unison directory if\n\
+      \032         profile \"name\" does not exist) and includes its contents as if\n\
+      \032         it was part of a profile or given directly on command line.\n\
+      \n\
       \032  key xxx\n\
       \032         Used in a profile to define a numeric key (0-9) that can be used\n\
       \032         in the user interface to switch immediately to this profile.\n\
@@ -1736,18 +1800,27 @@ let docs =
       \032  label xxx\n\
       \032         Used in a profile to provide a descriptive string documenting\n\
       \032         its settings. (This is useful for users that switch between\n\
-      \032         several profiles, especially using the `fast switch' feature of\n\
+      \032         several profiles, especially using the \226\128\152fast switch\226\128\153 feature of\n\
       \032         the graphical user interface.)\n\
       \n\
       \032  links xxx\n\
       \032         When set to true, this flag causes Unison to synchronize\n\
       \032         symbolic links. When the flag is set to false, symbolic links\n\
-      \032         will result in an error during update detection. Ordinarily,\n\
-      \032         when the flag is set to default, symbolic links are synchronized\n\
-      \032         except when one of the hosts is running Windows. On a Windows\n\
-      \032         client, Unison makes an attempt to detect if symbolic links are\n\
+      \032         will be ignored during update detection. Ordinarily, when the\n\
+      \032         flag is set to default, symbolic links are synchronized except\n\
+      \032         when one of the hosts is running Windows. On a Windows client,\n\
+      \032         Unison makes an attempt to detect if symbolic links are\n\
       \032         supported and allowed by user privileges. You may have to get\n\
-      \032         elevated privileges to create symbolic links.\n\
+      \032         elevated privileges to create symbolic links. When the flag is\n\
+      \032         set to default and symbolic links can\226\128\153t be synchronized then an\n\
+      \032         error is produced during update detection.\n\
+      \n\
+      \032  listen xxx\n\
+      \032         When acting as a server on a TCP socket, Unison will by default\n\
+      \032         listen on \"any\" address (0.0.0.0 and [::]). This command-line\n\
+      \032         argument allows to specify a different listening address and can\n\
+      \032         be repeated to listen on multiple addresses. Listening address\n\
+      \032         can be specified as a host name or an IP address.\n\
       \n\
       \032  log\n\
       \032         When this flag is set, Unison will log all changes to the\n\
@@ -1786,37 +1859,41 @@ let docs =
       \032         the transport phase. Normally, it should be set reasonably high\n\
       \032         to maximize performance, but when Unison is used over a\n\
       \032         low-bandwidth link it may be helpful to set it lower (e.g. to 1)\n\
-      \032         so that Unison doesn't soak up all the available bandwidth. The\n\
+      \032         so that Unison doesn\226\128\153t soak up all the available bandwidth. The\n\
       \032         default is the special value 0, which mean 20 threads when file\n\
-      \032         content streaming is desactivated and 1000 threads when it is\n\
+      \032         content streaming is deactivated and 1000 threads when it is\n\
       \032         activated.\n\
       \n\
       \032  merge xxx\n\
       \032         This preference can be used to run a merge program which will\n\
       \032         create a new version for each of the files and the backup, with\n\
       \032         the last backup and both replicas. The syntax of pathspec -> cmd\n\
-      \032         is described in the section \"Path Specification\" , and further\n\
-      \032         details on Merging functions are present in the section \"Merging\n\
-      \032         Conflicting Versions\" .\n\
+      \032         is described in the section \226\128\156Path Specification\226\128\157 , and further\n\
+      \032         details on Merging functions are present in the section \226\128\156Merging\n\
+      \032         Conflicting Versions\226\128\157 .\n\
       \n\
       \032  mountpoint xxx\n\
       \032         Including the preference -mountpoint PATH causes Unison to\n\
       \032         double-check, at the end of update detection, that PATH exists\n\
       \032         and abort if it does not. This is useful when Unison is used to\n\
       \032         synchronize removable media. This preference can be given more\n\
-      \032         than once. See the section \"Mount Points and Removable Media\" .\n\
+      \032         than once. See the section \226\128\156Mount Points and Removable Media\226\128\157 .\n\
       \n\
       \032  nocreation xxx\n\
       \032         Including the preference -nocreation root prevents Unison from\n\
       \032         performing any file creation on root root.\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         This preference can be included twice, once for each root, if\n\
       \032         you want to prevent any creation.\n\
       \n\
       \032  nocreationpartial xxx\n\
       \032         Including the preference nocreationpartial = PATHSPEC -> root\n\
       \032         prevents Unison from performing any file creation in PATHSPEC on\n\
-      \032         root root (see the section \"Path Specification\" for more\n\
+      \032         root root (see the section \226\128\156Path Specification\226\128\157 for more\n\
       \032         information). It is recommended to use BelowPath patterns when\n\
       \032         selecting a directory and all its contents.\n\
       \n\
@@ -1824,13 +1901,17 @@ let docs =
       \032         Including the preference -nodeletion root prevents Unison from\n\
       \032         performing any file deletion on root root.\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         This preference can be included twice, once for each root, if\n\
       \032         you want to prevent any deletion.\n\
       \n\
       \032  nodeletionpartial xxx\n\
       \032         Including the preference nodeletionpartial = PATHSPEC -> root\n\
       \032         prevents Unison from performing any file deletion in PATHSPEC on\n\
-      \032         root root (see the section \"Path Specification\" for more\n\
+      \032         root root (see the section \226\128\156Path Specification\226\128\157 for more\n\
       \032         information). It is recommended to use BelowPath patterns when\n\
       \032         selecting a directory and all its contents.\n\
       \n\
@@ -1838,13 +1919,17 @@ let docs =
       \032         Including the preference -noupdate root prevents Unison from\n\
       \032         performing any file update or deletion on root root.\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         This preference can be included twice, once for each root, if\n\
       \032         you want to prevent any update.\n\
       \n\
       \032  noupdatepartial xxx\n\
       \032         Including the preference noupdatepartial = PATHSPEC -> root\n\
       \032         prevents Unison from performing any file update or deletion in\n\
-      \032         PATHSPEC on root root (see the section \"Path Specification\" for\n\
+      \032         PATHSPEC on root root (see the section \226\128\156Path Specification\226\128\157 for\n\
       \032         more information). It is recommended to use BelowPath patterns\n\
       \032         when selecting a directory and all its contents.\n\
       \n\
@@ -1866,20 +1951,20 @@ let docs =
       \032         If one or more path preferences are given, then Unison will\n\
       \032         synchronize only these paths and their children. (This is useful\n\
       \032         for doing a fast sync of just one directory, for example.) Note\n\
-      \032         that path preferences are interpreted literally--they are not\n\
+      \032         that path preferences are interpreted literally\226\128\148they are not\n\
       \032         regular expressions.\n\
       \n\
       \032  perms n\n\
       \032         The integer value of this preference is a mask indicating which\n\
       \032         permission bits should be synchronized. It is set by default to\n\
       \032         0o1777: all bits but the set-uid and set-gid bits are\n\
-      \032         synchronised (synchronizing theses latter bits can be a security\n\
+      \032         synchronised (synchronizing these latter bits can be a security\n\
       \032         hazard). If you want to synchronize all bits, you can set the\n\
-      \032         value of this preference to -1. If one of the replica is on a\n\
-      \032         FAT [Windows] filesystem, you should consider using the t fat\n\
+      \032         value of this preference to \226\136\1461. If one of the replica is on a\n\
+      \032         FAT [Windows] filesystem, you should consider using the fat\n\
       \032         preference instead of this preference. If you need Unison not to\n\
       \032         set permissions at all, set the value of this preference to 0\n\
-      \032         and set the preference t dontchmod to t true.\n\
+      \032         and set the preference dontchmod to true.\n\
       \n\
       \032  prefer xxx\n\
       \032         Including the preference -prefer root causes Unison always to\n\
@@ -1888,6 +1973,10 @@ let docs =
       \032         preference merge. (The syntax of root is the same as for the\n\
       \032         root preference, plus the special values newer and older.)\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         This preference is overridden by the preferpartial preference.\n\
       \n\
       \032         This preference should be used only if you are sure you know\n\
@@ -1897,10 +1986,14 @@ let docs =
       \032         Including the preference preferpartial = PATHSPEC -> root causes\n\
       \032         Unison always to resolve conflicts in favor of root, rather than\n\
       \032         asking for guidance from the user, for the files in PATHSPEC\n\
-      \032         (see the section \"Path Specification\" for more information).\n\
+      \032         (see the section \226\128\156Path Specification\226\128\157 for more information).\n\
       \032         (The syntax of root is the same as for the root preference, plus\n\
       \032         the special values newer and older.)\n\
       \n\
+      \032         You can also specify a unique prefix or suffix of the path of\n\
+      \032         one of the roots or a unique prefix of the hostname of a remote\n\
+      \032         root.\n\
+      \n\
       \032         This preference should be used only if you are sure you know\n\
       \032         what you are doing!\n\
       \n\
@@ -1910,7 +2003,11 @@ let docs =
       \032         stopping. If the argument is a number, Unison will pause for\n\
       \032         that many seconds before beginning again. When the argument is\n\
       \032         watch, Unison relies on an external file monitoring process to\n\
-      \032         synchronize whenever a change happens.\n\
+      \032         synchronize whenever a change happens. You can combine the two\n\
+      \032         with a + character to use file monitoring and also do a full\n\
+      \032         scan every specificed number of seconds. For example, watch+3600\n\
+      \032         will react to changes immediately and additionally do a full\n\
+      \032         scan every hour.\n\
       \n\
       \032  retry n\n\
       \032         Setting this preference causes the text-mode interface to try\n\
@@ -1923,11 +2020,11 @@ let docs =
       \032         needed, so normal modes of usage are either to give two values\n\
       \032         for root in the profile, or to give no values in the profile and\n\
       \032         provide two on the command line. Details of the syntax of roots\n\
-      \032         can be found in the section \"Roots\" .\n\
+      \032         can be found in the section \226\128\156Roots\226\128\157 .\n\
       \n\
       \032         The two roots can be given in either order; Unison will sort\n\
       \032         them into a canonical order before doing anything else. It also\n\
-      \032         tries to `canonize' the machine names and paths that appear in\n\
+      \032         tries to \226\128\152canonize\226\128\153 the machine names and paths that appear in\n\
       \032         the roots, so that, if Unison is invoked later with a slightly\n\
       \032         different name for the same root, it will be able to locate the\n\
       \032         correct archives.\n\
@@ -1937,16 +2034,6 @@ let docs =
       \032         of roots, Unison replaces any roots matching the left-hand side\n\
       \032         of any rootalias rule by the corresponding right-hand side.\n\
       \n\
-      \032  rshargs xxx\n\
-      \032         The string value of this preference will be passed as additional\n\
-      \032         arguments (besides the host name and the name of the Unison\n\
-      \032         executable on the remote system) to the rsh command used to\n\
-      \032         invoke the remote server. The backslash is an escape character.\n\
-      \n\
-      \032  rshcmd xxx\n\
-      \032         This preference can be used to explicitly set the name of the\n\
-      \032         rsh executable (e.g., giving a full path name), if necessary.\n\
-      \n\
       \032  rsrc xxx\n\
       \032         When set to true, this flag causes Unison to synchronize\n\
       \032         resource forks and HFS meta-data. On filesystems that do not\n\
@@ -1958,11 +2045,11 @@ let docs =
       \032         circumstances it is useful to set the flag manually.\n\
       \n\
       \032  rsync\n\
-      \032         Unison uses the 'rsync algorithm' for 'diffs-only' transfer of\n\
+      \032         Unison uses the \226\128\153rsync algorithm\226\128\153 for \226\128\153diffs-only\226\128\153 transfer of\n\
       \032         updates to large files. Setting this flag to false makes Unison\n\
       \032         use whole-file transfers instead. Under normal circumstances,\n\
       \032         there is no reason to do this, but if you are having trouble\n\
-      \032         with repeated 'rsync failure' errors, setting it to false should\n\
+      \032         with repeated \226\128\153rsync failure\226\128\153 errors, setting it to false should\n\
       \032         permit you to synchronize the offending files.\n\
       \n\
       \032  selftest\n\
@@ -1985,15 +2072,20 @@ let docs =
       \032         name), if necessary.\n\
       \n\
       \032  showarchive\n\
-      \032         When this preference is set, Unison will print out the 'true\n\
-      \032         names'of the roots, in the same form as is expected by the\n\
-      \032         rootaliaspreference.\n\
+      \032         When this preference is set, Unison will print out the \226\128\153true\n\
+      \032         names\226\128\153of the roots, in the same form as is expected by the\n\
+      \032         rootalias preference.\n\
       \n\
       \032  silent\n\
       \032         When this preference is set to true, the textual user interface\n\
       \032         will print nothing at all, except in the case of errors. Setting\n\
       \032         silent to true automatically sets the batch preference to true.\n\
       \n\
+      \032  socket xxx\n\
+      \032         Start unison as a server listening on a TCP socket (with TCP\n\
+      \032         port number as argument) or a local socket (aka Unix domain\n\
+      \032         socket) (with socket path as argument).\n\
+      \n\
       \032  sortbysize\n\
       \032         When this flag is set, the user interface will list changed\n\
       \032         files by size (smallest first) rather than by name. This is\n\
@@ -2003,14 +2095,14 @@ let docs =
       \n\
       \032         This preference (as well as the other sorting flags, but not the\n\
       \032         sorting preferences that require patterns as arguments) can be\n\
-      \032         set interactively and temporarily using the 'Sort' menu in the\n\
+      \032         set interactively and temporarily using the \226\128\153Sort\226\128\153 menu in the\n\
       \032         graphical and text user interfaces.\n\
       \n\
       \032  sortfirst xxx\n\
       \032         Each argument to sortfirst is a pattern pathspec, which\n\
       \032         describes a set of paths. Files matching any of these patterns\n\
       \032         will be listed first in the user interface. The syntax of\n\
-      \032         pathspec is described in the section \"Path Specification\" .\n\
+      \032         pathspec is described in the section \226\128\156Path Specification\226\128\157 .\n\
       \n\
       \032  sortlast xxx\n\
       \032         Similar to sortfirst, except that files matching one of these\n\
@@ -2019,9 +2111,14 @@ let docs =
       \032  sortnewfirst\n\
       \032         When this flag is set, the user interface will list newly\n\
       \032         created files before all others. This is useful, for example,\n\
-      \032         for checking that newly created files are not `junk', i.e., ones\n\
+      \032         for checking that newly created files are not \226\128\152junk\226\128\153, i.e., ones\n\
       \032         that should be ignored or deleted rather than synchronized.\n\
       \n\
+      \032  source xxx\n\
+      \032         Include preferences from a file. source name reads the file\n\
+      \032         \"name\" in the .unison directory and includes its contents as if\n\
+      \032         it was part of a profile or given directly on command line.\n\
+      \n\
       \032  sshargs xxx\n\
       \032         The string value of this preference will be passed as additional\n\
       \032         arguments (besides the host name and the name of the Unison\n\
@@ -2032,18 +2129,10 @@ let docs =
       \032         This preference can be used to explicitly set the name of the\n\
       \032         ssh executable (e.g., giving a full path name), if necessary.\n\
       \n\
-      \032  sshversion xxx\n\
-      \032         This preference can be used to control which version of ssh\n\
-      \032         should be used to connect to the server. Legal values are 1 and\n\
-      \032         2, which will cause unison to try to use ssh1 orssh2 instead of\n\
-      \032         just ssh to invoke ssh. The default value is empty, which will\n\
-      \032         make unison use whatever version of ssh is installed as the\n\
-      \032         default `ssh' command.\n\
-      \n\
       \032  stream\n\
-      \032         When this preference is set, Unison will use an experimental\n\
-      \032         streaming protocol for transferring file contents more\n\
-      \032         efficiently. The default value is true.\n\
+      \032         (Deprecated) When this preference is set, Unison will use an\n\
+      \032         experimental streaming protocol for transferring file contents\n\
+      \032         more efficiently. The default value is true.\n\
       \n\
       \032  terse\n\
       \032         When this preference is set to true, the user interface will not\n\
@@ -2063,7 +2152,7 @@ let docs =
       \032         This preference selects either the graphical or the textual user\n\
       \032         interface. Legal values are graphic or text.\n\
       \n\
-      \032         Because this option is processed specially during Unison's\n\
+      \032         Because this option is processed specially during Unison\226\128\153s\n\
       \032         start-up sequence, it can only be used on the command line. In\n\
       \032         preference files it has no effect.\n\
       \n\
@@ -2074,13 +2163,13 @@ let docs =
       \032  unicode xxx\n\
       \032         When set to true, this flag causes Unison to perform case\n\
       \032         insensitive file comparisons assuming Unicode encoding. This is\n\
-      \032         the default. When the flag is set to false, a Latin 1 encoding\n\
-      \032         is assumed. When Unison runs in case sensitive mode, this flag\n\
-      \032         only makes a difference if one host is running Windows or Mac OS\n\
-      \032         X. Under Windows, the flag selects between using the Unicode or\n\
-      \032         8bit Windows API for accessing the filesystem. Under Mac OS X,\n\
-      \032         it selects whether comparing the filenames up to decomposition,\n\
-      \032         or byte-for-byte.\n\
+      \032         the default. When the flag is set to false, Latin 1 encoding is\n\
+      \032         assumed (this means that all bytes that are not letters in Latin\n\
+      \032         1 encoding will be compared byte-for-byte, even if they may be\n\
+      \032         valid characters in some other encoding). When Unison runs in\n\
+      \032         case sensitive mode, this flag only makes a difference if one\n\
+      \032         host is running Mac OS X. Under Mac OS X, it selects whether\n\
+      \032         comparing the filenames up to decomposition, or byte-for-byte.\n\
       \n\
       \032  version\n\
       \032         Print the current version number and exit. (This option only\n\
@@ -2091,12 +2180,57 @@ let docs =
       \032         filesystem changes; this is used to speed up update detection.\n\
       \032         Setting this flag to false disables the use of this process.\n\
       \n\
+      \032  xattrignore xxx\n\
+      \032         Preference -xattrignore namespec causes Unison to ignore\n\
+      \032         extended attributes with names that match namespec. This can be\n\
+      \032         used to exclude extended attributes that would fail\n\
+      \032         synchronization due to lack of permissions or technical\n\
+      \032         differences at replicas. The syntax of namespec is the same as\n\
+      \032         used for path specification (described in the section \226\128\156Path\n\
+      \032         Specification\226\128\157 ); prefer the Path and Regex forms over the Name\n\
+      \032         form. The pattern is applied to the name of extended attribute,\n\
+      \032         not to path. On Linux, attributes in the security and trusted\n\
+      \032         namespaces are ignored by default (this is achieved by pattern\n\
+      \032         Regex !(security|trusted)[.].*); also attributes used to store\n\
+      \032         POSIX ACL are ignored by default (this is achieved by pattern\n\
+      \032         Path !system.posix_acl_*). To sync attributes in one or both of\n\
+      \032         these namespaces, see the xattrignorenot preference. Note that\n\
+      \032         the namespace name must be prefixed with a \"!\" (applies on Linux\n\
+      \032         only). All names not prefixed with a \"!\" are taken as strictly\n\
+      \032         belonging to the user namespace and therefore the \"!user.\"\n\
+      \032         prefix is never used.\n\
+      \n\
+      \032  xattrignorenot xxx\n\
+      \032         This preference overrides the preference xattrignore. It gives a\n\
+      \032         list of patterns (in the same format as xattrignore) for\n\
+      \032         extended attributes that should not be ignored, whether or not\n\
+      \032         they happen to match one of the xattrignore patterns. It is\n\
+      \032         possible to synchronize only desired attributes by ignoring all\n\
+      \032         attributes (for example, by setting xattrignore to Path * and\n\
+      \032         then adding xattrignorenot for extended attributes that should\n\
+      \032         be synchronized. On Linux, attributes in the security and\n\
+      \032         trusted namespaces are ignored by default. To sync attributes in\n\
+      \032         one or both of these namespaces, you may add an xattrignorenot\n\
+      \032         pattern like Path !security.* to sync all attributes in the\n\
+      \032         security namespace, or Path !security.selinux to sync a specific\n\
+      \032         attribute in an otherwise ignored namespace. A pattern like Path\n\
+      \032         !system.posix_acl_* can be used to sync POSIX ACLs on Linux.\n\
+      \032         Note that the namespace name must be prefixed with a \"!\"\n\
+      \032         (applies on Linux only). All names not prefixed with a \"!\" are\n\
+      \032         taken as strictly belonging to the user namespace and therefore\n\
+      \032         the \"!user.\" prefix is never used.\n\
+      \n\
+      \032  xattrs\n\
+      \032         When this flag is set to true, the extended attributes of files\n\
+      \032         and directories are synchronized. System extended attributes are\n\
+      \032         not synchronized.\n\
+      \n\
       \032  xferbycopying\n\
       \032         When this preference is set, Unison will try to avoid\n\
       \032         transferring file contents across the network by recognizing\n\
       \032         when a file with the required contents already exists in the\n\
       \032         target replica. This usually allows file moves to be propagated\n\
-      \032         very quickly. The default value istrue.\n\
+      \032         very quickly. The default value is true.\n\
       \n\
       Profiles\n\
       \n\
@@ -2121,8 +2255,14 @@ let docs =
       \n\
       \032  for a preference of any other type.\n\
       \n\
-      \032  Whitespaces around p and xxx are ignored. A profile may also include\n\
-      \032  blank lines and lines beginning with #; both are ignored.\n\
+      \032  A profile may include blank lines and lines beginning with #; both are\n\
+      \032  ignored.\n\
+      \n\
+      \032  Spaces and tabs before and after p and xxx are ignored. Spaces, tabs,\n\
+      \032  and non-printable characters within values are not treated specially,\n\
+      \032  so that e.g. root = /foo bar refers to a directory containing a space.\n\
+      \032  (On systems using newline for line ending, carriage returns are\n\
+      \032  currently ignored, but this is not part of the specification.)\n\
       \n\
       \032  When Unison starts, it first reads the profile and then the command\n\
       \032  line, so command-line options will override settings from the profile.\n\
@@ -2135,10 +2275,10 @@ let docs =
       \032  similar line of the form source name does the same except that it does\n\
       \032  not attempt to add a suffix to name. Similar lines of the form include?\n\
       \032  name or source? name do the same as their respective lines without the\n\
-      \032  question mark except that it does not constitue an error to specify a\n\
+      \032  question mark except that it does not constitute an error to specify a\n\
       \032  non-existing file name. In name the backslash is an escape character.\n\
       \n\
-      \032  A profile may include a preference `label = desc' to provide a\n\
+      \032  A profile may include a preference \226\128\152label = desc\226\128\153 to provide a\n\
       \032  description of the options selected in this profile. The string desc is\n\
       \032  listed along with the profile name in the profile selection dialog, and\n\
       \032  displayed in the top-right corner of the main Unison window in the\n\
@@ -2146,7 +2286,7 @@ let docs =
       \n\
       \032  The graphical user-interface also supports one-key shortcuts for\n\
       \032  commonly used profiles. If a profile contains a preference of the form\n\
-      \032  `key = n', where n is a single digit, then pressing this digit key will\n\
+      \032  \226\128\152key = n\226\128\153, where n is a single digit, then pressing this digit key will\n\
       \032  cause Unison to immediately switch to this profile and begin\n\
       \032  synchronization again from scratch. In this case, all actions that have\n\
       \032  been selected for a set of changes currently being displayed will be\n\
@@ -2209,8 +2349,8 @@ let docs =
       \032  When Unison is used with large replicas, it is often convenient to be\n\
       \032  able to synchronize just a part of the replicas on a given run (this\n\
       \032  saves the time of detecting updates in the other parts). This can be\n\
-      \032  accomplished by splitting up the profile into several parts -- a common\n\
-      \032  part containing most of the preference settings, plus one \"top-level\"\n\
+      \032  accomplished by splitting up the profile into several parts \226\128\148 a common\n\
+      \032  part containing most of the preference settings, plus one \226\128\156top-level\226\128\157\n\
       \032  file for each set of paths that need to be synchronized. (The include\n\
       \032  mechanism can also be used to allow the same set of preference settings\n\
       \032  to be used with different roots.)\n\
@@ -2245,14 +2385,14 @@ let docs =
       \032   ignore = Name *.tmp\n\
       \n\
       \032  Note that there are no path preferences in common. This means that,\n\
-      \032  when we invoke Unison with the default profile (e.g., by typing 'unison\n\
-      \032  default' or just 'unison' on the command line), the whole replicas will\n\
+      \032  when we invoke Unison with the default profile (e.g., by typing \226\128\153unison\n\
+      \032  default\226\128\153 or just \226\128\153unison\226\128\153 on the command line), the whole replicas will\n\
       \032  be synchronized. (If we never want to synchronize the whole replicas,\n\
       \032  then default.prf would instead include settings for all the paths that\n\
       \032  are usually synchronized.)\n\
       \n\
       \032  To synchronize just part of the replicas, Unison is invoked with an\n\
-      \032  alternate preference file--e.g., doing 'unison workingset', where the\n\
+      \032  alternate preference file\226\128\148e.g., doing \226\128\153unison workingset\226\128\153, where the\n\
       \032  preference file workingset.prf contains\n\
       \032   path = current/papers\n\
       \032   path = Mail/inbox\n\
@@ -2288,7 +2428,7 @@ let docs =
       \032  example,\n\
       \032   backup = Name *\n\
       \n\
-      \032  causes Unison to keep backups of all files and directories. The\n\
+      \032  causes Unison to create backups of all files and directories. The\n\
       \032  backupnot preference can be used to give a few exceptions: it specifies\n\
       \032  which files and directories should not be backed up, even if they match\n\
       \032  the backup pathspec.\n\
@@ -2304,7 +2444,10 @@ let docs =
       \n\
       \032  Backup files can be stored either centrally or locally. This behavior\n\
       \032  is controlled by the preference backuplocation, whose value must be\n\
-      \032  either central or local. (The default is central.)\n\
+      \032  either central or local. (The default is central.) Note that central\n\
+      \032  storage of backups can lead to backup files being stored in a different\n\
+      \032  filesystem than the original files, which could have different security\n\
+      \032  properties and different amounts of available storage.\n\
       \n\
       \032  When backups are stored locally, they are kept in the same directory as\n\
       \032  the original.\n\
@@ -2313,10 +2456,12 @@ let docs =
       \032  controlled by the preference backupdir and the environment variable\n\
       \032  UNISONBACKUPDIR. (The environment variable is checked first.) If\n\
       \032  neither of these are set, then the directory .unison/backup in the\n\
-      \032  user's home directory is used.\n\
+      \032  user\226\128\153s home directory is used.\n\
       \n\
-      \032  The preference maxbackups controls how many previous versions of each\n\
-      \032  file are kept (including the current version).\n\
+      \032  The preference maxbackups (default 2) controls how many previous\n\
+      \032  versions of each file are kept (including the current version),\n\
+      \032  following the usual plan of deleting the oldest when creating a new\n\
+      \032  one.\n\
       \n\
       \032  By default, backup files are named .bak.VERSION.FILENAME, where\n\
       \032  FILENAME is the original filename and VERSION is the backup number (1\n\
@@ -2335,6 +2480,9 @@ let docs =
       \032  This can be used, for example, to ensure that backup files retain the\n\
       \032  same extension as the originals.\n\
       \n\
+      \032  Other than maxbackups (which will never delete the last backup), there\n\
+      \032  are no other mechanisms for deleting backups.\n\
+      \n\
       \032  For backward compatibility, the backups preference is also supported.\n\
       \032  It simply means backup = Name * and backuplocation = local.\n\
       \n\
@@ -2350,8 +2498,8 @@ let docs =
       \032   merge = <PATHSPEC> -> <MERGECMD>\n\
       \n\
       \032  The <PATHSPEC> here has exactly the same format as for the ignore\n\
-      \032  preference (see the section \"Path Specification\" ). For example, using\n\
-      \032  \"Name *.txt\" as the <PATHSPEC> tells Unison that this command should be\n\
+      \032  preference (see the section \226\128\156Path Specification\226\128\157 ). For example, using\n\
+      \032  \226\128\156Name *.txt\226\128\157 as the <PATHSPEC> tells Unison that this command should be\n\
       \032  used whenever a file with extension .txt needs to be merged.\n\
       \n\
       \032  Many external merging programs require as inputs not just the two files\n\
@@ -2359,14 +2507,16 @@ let docs =
       \032  synchronized version. You can ask Unison to keep a copy of the last\n\
       \032  synchronized version for some files using the backupcurrent preference.\n\
       \032  This preference is used in exactly the same way as backup and its\n\
-      \032  meaning is similar, except that it causes backups to be kept of the\n\
+      \032  meaning is similar, except that it causes backups to be created of the\n\
       \032  current contents of each file after it has been synchronized by Unison,\n\
       \032  rather than the previous contents that Unison overwrote. These backups\n\
-      \032  are kept on both replicas in the same place as ordinary backup\n\
-      \032  files--i.e. according to the backuplocation and backupdir preferences.\n\
+      \032  are stored in both replicas in the same place as ordinary backup\n\
+      \032  files\226\128\148i.e. according to the backuplocation and backupdir preferences.\n\
       \032  They are named like the original files if backupslocation is set to\n\
-      \032  'central' and otherwise, Unison uses the backupprefix and backupsuffix\n\
-      \032  preferences and assumes a version number 000 for these backups.\n\
+      \032  \226\128\153central\226\128\153 and otherwise, Unison uses the backupprefix and backupsuffix\n\
+      \032  preferences and assumes a version number 000 for these backups. Note\n\
+      \032  that there are no mechanisms (beyond the limit on the number of backups\n\
+      \032  for each file) to remove backup files.\n\
       \n\
       \032  The <MERGECMD> part of the preference specifies what external command\n\
       \032  should be invoked to merge files at paths matching the <PATHSPEC>.\n\
@@ -2397,11 +2547,11 @@ let docs =
       \032      Unison expects to be written by the merge program when it is only\n\
       \032      able to partially merge the originals; in this case, NEW1 will be\n\
       \032      written back to the local replica and NEW2 to the remote replica;\n\
-      \032      NEWARCH, if present, will be used as the \"last common state\" of the\n\
+      \032      NEWARCH, if present, will be used as the \226\128\156last common state\226\128\157 of the\n\
       \032      replicas. (These three options are provided for later compatibility\n\
       \032      with the Harmony data synchronizer.)\n\
       \032    * BATCHMODE is replaced according to the batch mode of Unison; if it\n\
-      \032      is in batch mode, then a non empty string (\"batch\") is substituted,\n\
+      \032      is in batch mode, then a non empty string (\226\128\156batch\226\128\157) is substituted,\n\
       \032      otherwise the empty string is substituted.\n\
       \n\
       \032  To accommodate the wide variety of programs that users might want to\n\
@@ -2435,7 +2585,7 @@ let docs =
       \032      merge command only synchronizes some specific contents between two\n\
       \032      files, skipping some irrelevant information (order between entries,\n\
       \032      for instance). We assume that, if the merge command exits normally,\n\
-      \032      then the two resulting files are \"as good as equal.\" (The reason we\n\
+      \032      then the two resulting files are \226\128\156as good as equal.\226\128\157 (The reason we\n\
       \032      copy one on top of the other is to avoid Unison detecting that the\n\
       \032      files are unequal the next time it is run and trying again to merge\n\
       \032      them when, in fact, the merge program has already made them as\n\
@@ -2500,7 +2650,7 @@ let docs =
       \032  preference instead of merge.\n\
       \n\
       \032    Please post suggestions for other useful values of the merge\n\
-      \032    preference to the unison-users mailing list--we'd like to give\n\
+      \032    preference to the unison-users mailing list\226\128\148we\226\128\153d like to give\n\
       \032    several examples here.\n\
       \n\
       The User Interface\n\
@@ -2508,15 +2658,51 @@ let docs =
       \032  Both the textual and the graphical user interfaces are intended to be\n\
       \032  mostly self-explanatory. Here are just a few tricks:\n\
       \032    * By default, when running on Unix the textual user interface will\n\
-      \032      try to put the terminal into the \"raw mode\" so that it reads the\n\
+      \032      try to put the terminal into the \226\128\156raw mode\226\128\157 so that it reads the\n\
       \032      input a character at a time rather than a line at a time. (This\n\
-      \032      means you can type just the single keystroke \">\" to tell Unison to\n\
-      \032      propagate a file from left to right, rather than \"> Enter.\")\n\
-      \032      There are some situations, though, where this will not work -- for\n\
+      \032      means you can type just the single keystroke \226\128\156>\226\128\157 to tell Unison to\n\
+      \032      propagate a file from left to right, rather than \226\128\156> Enter.\226\128\157)\n\
+      \032      There are some situations, though, where this will not work \226\128\148 for\n\
       \032      example, when Unison is running in a shell window inside Emacs.\n\
       \032      Setting the dumbtty preference will force Unison to leave the\n\
       \032      terminal alone and process input a line at a time.\n\
       \n\
+      Interrupting a Synchronization\n\
+      \n\
+      \032  It is possible to interrupt an ongoing synchronization process before\n\
+      \032  it completes. Different user interfaces offer different ways of doing\n\
+      \032  it.\n\
+      \n\
+      \032  Graphical Interface:\n\
+      \032    * In the graphical user interface the synchronization process can be\n\
+      \032      interrupted before it is finished by pressing the \226\128\156Stop\226\128\157 button or\n\
+      \032      by closing the window. The \226\128\156Stop\226\128\157 button causes the onging\n\
+      \032      propagation to be stopped as quickly as possible while still doing\n\
+      \032      proper cleanup. The application keeps running and a rescan can be\n\
+      \032      performed or a different profile selected. Closing the window in\n\
+      \032      the middle of update propagation process will exit the application\n\
+      \032      immediately without doing proper cleanup; it is therefore not\n\
+      \032      recommended unless the \226\128\156Stop\226\128\157 button does not react quickly enough.\n\
+      \n\
+      \032  Textual Interface:\n\
+      \032    * When not synchronizing continuously, the text interface terminates\n\
+      \032      when synchronization is finished normally or due to a fatal error\n\
+      \032      occurring.\n\
+      \032      In the text interface, to interrupt synchronization before it is\n\
+      \032      finished, press \226\128\156Ctrl-C\226\128\157 (or send signal SIGINT or SIGTERM). This\n\
+      \032      will interrupt update propagation as quickly as possible but still\n\
+      \032      complete proper cleanup. If the process does not stop even after\n\
+      \032      pressing \226\128\156Ctrl-C\226\128\157 then keep doing it repeatedly. This will bypass\n\
+      \032      cleanup procedures and terminates the process forcibly (similar to\n\
+      \032      SIGKILL). Doing so may leave the archives or replicas in an\n\
+      \032      inconsistent state or locked.\n\
+      \032      When synchronizing continuously (time interval repeat or with\n\
+      \032      filesystem monitoring), interrupting with \226\128\156Ctrl-C\226\128\157 or with signal\n\
+      \032      SIGINT or SIGTERM works the same way as described above and will\n\
+      \032      additionally stop the continuous process. To stop only the\n\
+      \032      continuous process and let the last synchronization complete\n\
+      \032      normally, send signal SIGUSR2 instead.\n\
+      \n\
       Exit Code\n\
       \n\
       \032  When running in the textual mode, Unison returns an exit status, which\n\
@@ -2563,7 +2749,7 @@ let docs =
       \n\
       \032      matches the path path and any path below. The name and path\n\
       \032      arguments of the latter forms of patterns are not regular\n\
-      \032      expressions. Instead, standard \"globbing\" conventions can be used\n\
+      \032      expressions. Instead, standard \226\128\156globbing\226\128\157 conventions can be used\n\
       \032      in name and path:\n\
       \032         + a * matches any sequence of characters not including / (and\n\
       \032           not beginning with ., when used at the beginning of a name)\n\
@@ -2573,11 +2759,11 @@ let docs =
       \032           to put extra spaces after the commas: these will be\n\
       \032           interpreted literally as part of the strings to be matched!)\n\
       \032    * The path separator in path patterns is always the forward-slash\n\
-      \032      character \"/\" -- even when the client or server is running under\n\
+      \032      character \226\128\156/\226\128\157 \226\128\148 even when the client or server is running under\n\
       \032      Windows, where the normal separator character is a backslash. This\n\
       \032      makes it possible to use the same set of path patterns for both\n\
       \032      Unix and Windows file systems.\n\
-      \032    * A path specification may be followed by the separator \" -> \" itself\n\
+      \032    * A path specification may be followed by the separator \226\128\156 -> \226\128\157 itself\n\
       \032      followed by a string which will be associated to the matching\n\
       \032      paths:\n\
       \032                Path path -> associated string\n\
@@ -2590,16 +2776,16 @@ let docs =
       \032      is not used. The associated string cannot contain the separator\n\
       \032      string.\n\
       \n\
-      \032  Some examples of path patterns appear in the section \"Ignoring Paths\" .\n\
+      \032  Some examples of path patterns appear in the section \226\128\156Ignoring Paths\226\128\157 .\n\
       \032  Associated strings are used by the preference merge.\n\
       \n\
       Ignoring Paths\n\
       \n\
       \032  Most users of Unison will find that their replicas contain lots of\n\
-      \032  files that they don't ever want to synchronize -- temporary files, very\n\
+      \032  files that they don\226\128\153t ever want to synchronize \226\128\148 temporary files, very\n\
       \032  large files, old stuff, architecture-specific binaries, etc. They can\n\
       \032  instruct Unison to ignore these paths using patterns introduced in the\n\
-      \032  section \"Path Specification\" .\n\
+      \032  section \226\128\156Path Specification\226\128\157 .\n\
       \n\
       \032  For example, the following pattern will make Unison ignore any path\n\
       \032  containing the name CVS or a name ending in .cmo:\n\
@@ -2619,17 +2805,17 @@ let docs =
       \032  and ending with a name ending by .ml.\n\
       \032            ignore = Regex a/b/.*\\.ml\n\
       \n\
-      \032  Note that regular expression patterns are \"anchored\": they must match\n\
+      \032  Note that regular expression patterns are \226\128\156anchored\226\128\157: they must match\n\
       \032  the whole path, not just a substring of the path.\n\
       \n\
       \032  Here are a few extra points regarding the ignore preference.\n\
       \032    * If a directory is ignored, all its descendants will be too.\n\
       \032    * The user interface provides some convenient commands for adding new\n\
       \032      patterns to be ignored. To ignore a particular file, select it and\n\
-      \032      press \"i\". To ignore all files with the same extension, select it\n\
-      \032      and press \"E\" (with the shift key). To ignore all files with the\n\
+      \032      press \226\128\156i\226\128\157. To ignore all files with the same extension, select it\n\
+      \032      and press \226\128\156E\226\128\157 (with the shift key). To ignore all files with the\n\
       \032      same name, no matter what directory they appear in, select it and\n\
-      \032      press \"N\". These new patterns become permanent: they are\n\
+      \032      press \226\128\156N\226\128\157. These new patterns become permanent: they are\n\
       \032      immediately added to the current profile on disk.\n\
       \032    * If you use the include directive to include a common collection of\n\
       \032      preferences in several top-level preference files, you will\n\
@@ -2650,7 +2836,7 @@ let docs =
       \032      an ignore pattern. However, the interaction of these two sets of\n\
       \032      patterns can be a little tricky. Here is exactly how it works:\n\
       \032         + Unison starts detecting updates from the root of the\n\
-      \032           replicas--i.e., from the empty path. If the empty path matches\n\
+      \032           replicas\226\128\148i.e., from the empty path. If the empty path matches\n\
       \032           an ignore pattern and does not match an ignorenot pattern,\n\
       \032           then the whole replica will be ignored. (For this reason, it\n\
       \032           is not a good idea to include Name * as an ignore pattern. If\n\
@@ -2666,19 +2852,19 @@ let docs =
       \n\
       Symbolic Links\n\
       \n\
-      \032  Ordinarily, Unison treats symbolic links in Unix replicas as \"opaque\":\n\
+      \032  Ordinarily, Unison treats symbolic links in Unix replicas as \226\128\156opaque\226\128\157:\n\
       \032  it considers the contents of the link to be just the string specifying\n\
       \032  where the link points, and it will propagate changes in this string to\n\
       \032  the other replica.\n\
       \n\
-      \032  It is sometimes useful to treat a symbolic link \"transparently,\" acting\n\
+      \032  It is sometimes useful to treat a symbolic link \226\128\156transparently,\226\128\157 acting\n\
       \032  as though whatever it points to were physically in the replica at the\n\
       \032  point where the symbolic link appears. To tell Unison to treat a link\n\
       \032  in this manner, add a line of the form\n\
       \032            follow = pathspec\n\
       \n\
       \032  to the profile, where pathspec is a path pattern as described in the\n\
-      \032  section \"Path Specification\" .\n\
+      \032  section \226\128\156Path Specification\226\128\157 .\n\
       \n\
       \032  Not all Windows versions and file systems support symbolic links;\n\
       \032  Unison will refuse to propagate an opaque symbolic link from Unix to\n\
@@ -2697,7 +2883,7 @@ let docs =
       \n\
       \032  Synchronizing the permission bits of files is slightly tricky when two\n\
       \032  different filesystems are involved (e.g., when synchronizing a Windows\n\
-      \032  client and a Unix server). In detail, here's how it works:\n\
+      \032  client and a Unix server). In detail, here\226\128\153s how it works:\n\
       \032    * When the permission bits of an existing file or directory are\n\
       \032      changed, the values of those bits that make sense on both operating\n\
       \032      systems will be propagated to the other replica. The other bits\n\
@@ -2709,9 +2895,143 @@ let docs =
       \032      Unix system).\n\
       \032    * For security reasons, the Unix setuid and setgid bits are not\n\
       \032      propagated.\n\
-      \032    * The Unix owner and group ids are not propagated. (What would this\n\
-      \032      mean, in general?) All files are created with the owner and group\n\
-      \032      of the server process.\n\
+      \032    * The Unix owner and group ids can be propagated (see owner and group\n\
+      \032      preferences) by mapping names or by numeric ids (see numericids\n\
+      \032      preference).\n\
+      \n\
+      Access Control Lists - ACLs\n\
+      \n\
+      \032  Unison allows synchronizing access control lists (ACLs) on platforms\n\
+      \032  and filesystems that support them. In general, synchronization makes\n\
+      \032  sense only in case both replicas support the same type of ACLs and\n\
+      \032  recognize same users and groups. In some cases you may be able to go\n\
+      \032  beyond that and synchronize ACLs to a replica that couldn\226\128\153t fully use\n\
+      \032  them\226\128\148this may be be useful for the purpose of preserving ACLs.\n\
+      \n\
+      \032  If one of the replicas does not support any type of ACLs then Unison\n\
+      \032  will not attempt ACL synchronization. If the other replica does support\n\
+      \032  ACLs then those will remain intact.\n\
+      \n\
+      \032  If both replicas support ACLs of any supported type then you can\n\
+      \032  request Unison to try ACL synchronization (acl preference). Success of\n\
+      \032  synchronization depends on permissions of the owner and group of Unison\n\
+      \032  process (Unison must have permissions to set ACL) and the compatibility\n\
+      \032  of ACL types on both replicas.\n\
+      \n\
+      \032  An ACL is propagated as a single unit, with all ACEs. There is no\n\
+      \032  merging of ACEs from the replicas.\n\
+      \n\
+      \032  Caveat: ACE inheritance may in certain scenarios cause synchronization\n\
+      \032  inconsistencies. In Windows, only explicit ACEs are synchronized;\n\
+      \032  inherited ACEs are not actively synchronized, but Windows will\n\
+      \032  propagate ACEs from parent directories (unless inheritance is\n\
+      \032  explicitly prevented on a file or a directory\226\128\148this prevention is also\n\
+      \032  synchronized). Due to inheritance, the ultimately effective ACL may be\n\
+      \032  different, or provide different access, even after synchronization.\n\
+      \n\
+      \032  Unison currently supports the following platforms and ACL types:\n\
+      \032    * Windows (Windows XP SP2 and later)\n\
+      \032         + NTFS ACL (discrete ACL (DACL) only)\n\
+      \032    * Solaris, OpenSolaris and illumos-based OS (OpenIndiana, SmartOS,\n\
+      \032      OmniOS, etc.)\n\
+      \032         + NFSv4 ACL (ZFS ACL)\n\
+      \032         + POSIX-draft ACL\n\
+      \032         + Some NFSv4 ACL (ZFS ACL) cross-synchronization with\n\
+      \032           POSIX-draft ACL\n\
+      \032         + Full cross-synchronization with other platforms that support\n\
+      \032           NFSv4 ACLs; limited cross-synchronization with POSIX-draft\n\
+      \032           ACLs\n\
+      \032    * FreeBSD, NetBSD\n\
+      \032         + NFSv4 ACL (ZFS ACL)\n\
+      \032         + Limited POSIX-draft ACL (access ACL only; not default ACL)\n\
+      \032         + Full cross-synchronization with other platforms that support\n\
+      \032           NFSv4 ACLs\n\
+      \032    * Darwin (macOS)\n\
+      \032         + Extended ACL\n\
+      \n\
+      \032  Not all filesystems on the listed platforms support all ACL types (or\n\
+      \032  any ACLs at all).\n\
+      \n\
+      \032  Synchronizing POSIX ACLs on Linux is not supported directly. However,\n\
+      \032  it is possible to synchronize these ACLs with another Linux system by\n\
+      \032  synchronizing extended attributes (xattrs) instead, because POSIX ACLs\n\
+      \032  are stored as xattrs by Linux. This is disabled by default (see the\n\
+      \032  section \226\128\156Extended Attributes - xattrs\226\128\157 ). A simple way to enable\n\
+      \032  syncing POSIX ACLs on Linux is to enable the preference xattrs and add\n\
+      \032  a preference xattrignorenot with a value Path !system.posix_acl_*. The\n\
+      \032  * will be expanded to include both posix_acl_access and\n\
+      \032  posix_acl_default attributes \226\128\147 if you only want to sync either one,\n\
+      \032  just remove the * and type out the attribute name in full. If you want\n\
+      \032  to prevent other xattrs from being synced then add an xattrignore with\n\
+      \032  a value Path * (value Regex .* will also work).\n\
+      \n\
+      Extended Attributes - xattrs\n\
+      \n\
+      \032  Unison allows synchronizing extended attributes on platforms and\n\
+      \032  filesystems that support them. System attributes are not synchronized.\n\
+      \032  What exactly is considered a system attribute is platform-dependent.\n\
+      \032  Synchronization is possible cross-platform, but see caveats below.\n\
+      \n\
+      \032  If one of the replicas does not support extended attributes then Unison\n\
+      \032  will not attempt attribute synchronization. If the other replica does\n\
+      \032  support extended attributes then those will remain intact.\n\
+      \n\
+      \032  If both replicas support extended attributes then you can request\n\
+      \032  Unison to try attribute synchronization (xattrs preference). Extended\n\
+      \032  attributes from both replicas will not be merged, all extended\n\
+      \032  attributes are propagated as a set from one replica to another.\n\
+      \n\
+      \032  Unison currently supports extended attributes on the following\n\
+      \032  platforms:\n\
+      \032    * Linux Attributes in user, trusted and security namespaces.\n\
+      \032      Synchronization of the latter two namespaces depends on unison\n\
+      \032      process privileges and is disabled by default. To sync one or more\n\
+      \032      attributes in the security namespace, for example, you can set the\n\
+      \032      preference xattrignorenot to Path !security.* (for all) or to Path\n\
+      \032      !security.selinux (for one specific attribute). Attributes in\n\
+      \032      system namespace are not synchronized, with the exception of\n\
+      \032      system.posix_acl_default and system.posix_acl_access (also disabled\n\
+      \032      by default).\n\
+      \032    * Solaris, OpenSolaris and illumos-based OS (OpenIndiana, SmartOS,\n\
+      \032      OmniOS, etc.)\n\
+      \032    * FreeBSD, NetBSD Attributes in user namespace.\n\
+      \032    * Darwin (macOS)\n\
+      \n\
+      \032  Not all filesystems on the listed platforms may support extended\n\
+      \032  attributes.\n\
+      \n\
+      \032  Caveats:\n\
+      \032    * Some platforms and file systems support very large extended\n\
+      \032      attribute values. Unison synchronizes only up to 16 MB of each\n\
+      \032      attribute value.\n\
+      \032    * Attributes are synchronized as simple name-value pairs. More\n\
+      \032      complex extended attribute concepts supported by some platforms are\n\
+      \032      not synchronized.\n\
+      \032    * On Linux, attribute names always have a fully qualified form\n\
+      \032      (namespace.attribute). Other platforms do not have the same\n\
+      \032      constraint. The consequence of this is that Unison will sync the\n\
+      \032      attribute names on Linux as follows: an ! is prepended to the\n\
+      \032      namespace name, except for the user namespace; the user. prefix is\n\
+      \032      stripped from attribute names instead. This allows syncing extended\n\
+      \032      attributes from Linux to other platforms. These transformations are\n\
+      \032      reversed when syncing to Linux, resulting in correct fully\n\
+      \032      qualified attribute names. The xattrignore and xattrignorenot\n\
+      \032      preferences work on the transformed attribute names. This means\n\
+      \032      that any patterns for the user namespace must be specified without\n\
+      \032      the user. prefix and any patterns intended for other namespaces\n\
+      \032      must begin with an !.\n\
+      \n\
+      \032  The xattrignore preference can be used to filter the names of extended\n\
+      \032  attributes that will be synchronized. The most useful ignore patterns\n\
+      \032  can be constructed with the Path form (where shell wildcards * and ?\n\
+      \032  are supported) and with the Regex form. The xattrignorenot preference\n\
+      \032  can be used to override xattrignore.\n\
+      \n\
+      \032  Disabling the security and trusted namespaces on Linux is achieved by\n\
+      \032  setting a default xattrignore pattern of Regex\n\
+      \032  !(security|trusted)[.].*. Disabling the syncing of attributes used to\n\
+      \032  store POSIX ACL on Linux is achieved by setting a default xattrignore\n\
+      \032  pattern of Path !system.posix_acl_*.\n\
       \n\
       Cross-Platform Synchronization\n\
       \n\
@@ -2722,23 +3042,23 @@ let docs =
       \032  refer to different files. In Windows, on the other hand, filenames are\n\
       \032  not case sensitive: foo and FOO can only refer to the same file. This\n\
       \032  means that a Unix foo and FOO cannot be synchronized onto a Windows\n\
-      \032  system -- Windows won't allow two different files to have the \"same\"\n\
+      \032  system \226\128\148 Windows won\226\128\153t allow two different files to have the \226\128\156same\226\128\157\n\
       \032  name. Unison detects this situation for you, and reports that it cannot\n\
       \032  synchronize the files.\n\
       \n\
       \032  You can deal with a case conflict in a couple of ways. If you need to\n\
       \032  have both files on the Windows system, your only choice is to rename\n\
       \032  one of the Unix files to avoid the case conflict, and re-synchronize.\n\
-      \032  If you don't need the files on the Windows system, you can simply\n\
-      \032  disregard Unison's warning message, and go ahead with the\n\
-      \032  synchronization; Unison won't touch those files. If you don't want to\n\
+      \032  If you don\226\128\153t need the files on the Windows system, you can simply\n\
+      \032  disregard Unison\226\128\153s warning message, and go ahead with the\n\
+      \032  synchronization; Unison won\226\128\153t touch those files. If you don\226\128\153t want to\n\
       \032  see the warning on each synchronization, you can tell Unison to ignore\n\
-      \032  the files (see the section \"Ignoring Paths\" ).\n\
+      \032  the files (see the section \226\128\156Ignoring Paths\226\128\157 ).\n\
       \n\
       \032  Illegal filenames. Unix allows some filenames that are illegal in\n\
-      \032  Windows. For example, colons (`:') are not allowed in Windows\n\
+      \032  Windows. For example, colons (\226\128\152:\226\128\153) are not allowed in Windows\n\
       \032  filenames, but they are legal in Unix filenames. This means that a Unix\n\
-      \032  file foo:bar can't be synchronized to a Windows system. As with case\n\
+      \032  file foo:bar can\226\128\153t be synchronized to a Windows system. As with case\n\
       \032  conflicts, Unison detects this situation for you, and you have the same\n\
       \032  options: you can either rename the Unix file and re-synchronize, or you\n\
       \032  can ignore it.\n\
@@ -2748,18 +3068,18 @@ let docs =
       \032  Unison is built to run well even over relatively slow links such as\n\
       \032  modems and DSL connections.\n\
       \n\
-      \032  Unison uses the \"rsync protocol\" designed by Andrew Tridgell and Paul\n\
+      \032  Unison uses the \226\128\156rsync protocol\226\128\157 designed by Andrew Tridgell and Paul\n\
       \032  Mackerras to greatly speed up transfers of large files in which only\n\
       \032  small changes have been made. More information about the rsync protocol\n\
       \032  can be found at the rsync web site (http://samba.anu.edu.au/rsync/).\n\
       \n\
       \032  If you are using Unison with ssh, you may get some speed improvement by\n\
-      \032  enabling ssh's compression feature. Do this by adding the option\n\
-      \032  \"-sshargs -C\" to the command line or \"sshargs = -C\" to your profile.\n\
+      \032  enabling ssh\226\128\153s compression feature. Do this by adding the option\n\
+      \032  \226\128\156-sshargs -C\226\128\157 to the command line or \226\128\156sshargs = -C\226\128\157 to your profile.\n\
       \n\
       Making Unison Faster on Large Files\n\
       \n\
-      \032  Unison's built-in implementation of the rsync algorithm makes\n\
+      \032  Unison\226\128\153s built-in implementation of the rsync algorithm makes\n\
       \032  transferring updates to existing files pretty fast. However, for\n\
       \032  whole-file copies of newly created files, the built-in transfer method\n\
       \032  is not highly optimized. Also, if Unison is interrupted in the middle\n\
@@ -2783,7 +3103,7 @@ let docs =
       \032  than a megabyte).\n\
       \n\
       \032  If you want to use a different external copy utility, set both the\n\
-      \032  copyprog and copyprogrest preferences--the former is used for the first\n\
+      \032  copyprog and copyprogrest preferences\226\128\148the former is used for the first\n\
       \032  transfer of a file, while the latter is used when Unison sees a\n\
       \032  partially transferred temp file on the receiving host. Be careful here:\n\
       \032  Your external tool needs to be instructed to copy files in place\n\
@@ -2793,37 +3113,29 @@ let docs =
       \032  copyprog      =   rsync --inplace --compress\n\
       \032  copyprogrest  =   rsync --partial --inplace --compress\n\
       \n\
-      \032  You may also need to set the copyquoterem preference. When it is set to\n\
-      \032  true, this causes Unison to add an extra layer of quotes to the remote\n\
-      \032  path passed to the external copy program. This is is needed by rsync,\n\
-      \032  for example, which internally uses an ssh connection, requiring an\n\
-      \032  extra level of quoting for paths containing spaces. When this flag is\n\
-      \032  set to default, extra quotes are added if the value of copyprog\n\
-      \032  contains the string rsync. The default value is default, naturally.\n\
-      \n\
       \032  If a directory transfer is interrupted, the next run of Unison will\n\
       \032  automatically skip any files that were completely transferred before\n\
       \032  the interruption. (This behavior is always on: it does not depend on\n\
       \032  the setting of the copythreshold preference.) Note, though, that the\n\
       \032  new directory will not appear in the destination filesystem until\n\
-      \032  everything has been transferred--partially transferred directories are\n\
+      \032  everything has been transferred\226\128\148partially transferred directories are\n\
       \032  kept in a temporary location (with names like .unison.DIRNAME....)\n\
       \032  until the transfer is complete.\n\
       \n\
       Fast Update Detection\n\
       \n\
       \032  If your replicas are large and at least one of them is on a Windows\n\
-      \032  system, you may find that Unison's default method for detecting changes\n\
+      \032  system, you may find that Unison\226\128\153s default method for detecting changes\n\
       \032  (which involves scanning the full contents of every file on every\n\
-      \032  sync--the only completely safe way to do it under Windows) is too slow.\n\
+      \032  sync\226\128\148the only completely safe way to do it under Windows) is too slow.\n\
       \032  Unison provides a preference fastcheck that, when set to true, causes\n\
-      \032  it to use file creation times as 'pseudo inode numbers' when scanning\n\
+      \032  it to use file creation times as \226\128\153pseudo inode numbers\226\128\153 when scanning\n\
       \032  replicas for updates, instead of reading the full contents of every\n\
       \032  file.\n\
       \n\
       \032  When fastcheck is set to no, Unison will perform slow\n\
-      \032  checking--re-scanning the contents of each file on each\n\
-      \032  synchronization--on all replicas. When fastcheck is set to default\n\
+      \032  checking\226\128\148re-scanning the contents of each file on each\n\
+      \032  synchronization\226\128\148on all replicas. When fastcheck is set to default\n\
       \032  (which, naturally, is the default), Unison will use fast checks on Unix\n\
       \032  replicas and slow checks on Windows replicas.\n\
       \n\
@@ -2847,8 +3159,7 @@ let docs =
       \032  you are careful. If you synchronize a directory that is stored on\n\
       \032  removable media when the media is not present, it will look to Unison\n\
       \032  as though the whole directory has been deleted, and it will proceed to\n\
-      \032  delete the directory from the other replica--probably not what you\n\
-      \032  want!\n\
+      \032  delete the directory from the other replica\226\128\148probably not what you want!\n\
       \n\
       \032  To prevent accidents, Unison provides a preference called mountpoint.\n\
       \032  Including a line like\n\
@@ -2868,1842 +3179,15 @@ let docs =
       \032  you want to use ssh.\n\
       \n\
       \032  When you click on the Unison icon, two windows will be created:\n\
-      \032  Unison's regular window, plus a console window, which is used only for\n\
+      \032  Unison\226\128\153s regular window, plus a console window, which is used only for\n\
       \032  giving your password to ssh (if you do not use ssh to connect, you can\n\
-      \032  ignore this window). When your password is requested, you'll need to\n\
+      \032  ignore this window). When your password is requested, you\226\128\153ll need to\n\
       \032  activate the console window (e.g., by clicking in it) before typing. If\n\
-      \032  you start Unison from a DOS window, Unison's regular window will appear\n\
+      \032  you start Unison from a DOS window, Unison\226\128\153s regular window will appear\n\
       \032  and you will type your password in the DOS window you were using.\n\
       \n\
       \032  To use Unison in this mode, you must first create a profile (see the\n\
-      \032  section \"Profiles\" ). Use your favorite editor for this.\n\
-      \n\
-      "))
-::
-    ("ssh", ("Ssh", 
-     "Ssh\n\
-      \n\
-      \032  Your local host will need just an ssh client; the remote host needs an\n\
-      \032  ssh server (or daemon). ssh is now normal, and Unison thus does not\n\
-      \032  provide instructions.\n\
-      \n\
-      "))
-::
-    ("news", ("Changes in Version 2.51.5", 
-     "Changes in Version 2.51.5\n\
-      \n\
-      \032  Changes since 2.51.4:\n\
-      \032    * Restore OCaml compat to before 4.02\n\
-      \032    * dune/opam improvements/fixes\n\
-      \032    * Improve GTK UI by using GtkTreeView\n\
-      \032    * Add support for syncing symlinks on Windows (NTFS)\n\
-      \032    * Improve ssh support on Windows (hide Windows console in GUI mode)\n\
-      \032    * Many bugfixes and minor improvements\n\
-      \n\
-      \032  Changes since 2.51.3:\n\
-      \032    * OCaml 4.12 support\n\
-      \032    * fsmonitor improvements and Solaris support\n\
-      \032    * Color support in text UI, with a new preference, disabled by\n\
-      \032      NO_COLOR.\n\
-      \032    * Interactive profile selection in text UI, enabled by a new\n\
-      \032      preference.\n\
-      \032    * Working files are stored in the unison directory (typically\n\
-      \032      /.unison) rather than $HOME.\n\
-      \032    * Build cleanups, CI improvements, housekeeping\n\
-      \032    * Many bugfixes and minor improvements\n\
-      \n\
-      \032  Changes since 2.51.2:\n\
-      \032    * Some nontrivial changes to profile parsing (G.raud Meyer)\n\
-      \032         + '=' has been considered whitespace until now: several\n\
-      \032           following chars are considered as only one; trailing chars are\n\
-      \032           discarded; any non empty sequence of char is splitting. This\n\
-      \032           is non standard and leads to confusion, for example -ignore==\n\
-      \032           'Name .*=*' is valid when -ignore='Name .*=*' is not, and\n\
-      \032           worse -ignore='Name *=' is the same as -ignore='Name *'. The\n\
-      \032           parser now takes just a single '=' as delimiter after the\n\
-      \032           option name. Other = characters are considered as part of the\n\
-      \032           value being assigned to the option.\n\
-      \032    * Numerous improvements to the text user-interface (G.raud Meyer)\n\
-      \032         + New key-commands that restrict the display to a set of\n\
-      \032           \"matching\" items: ones that are offering to propagate changes\n\
-      \032           in a particular direction, conflicts, files to be merged,\n\
-      \032           etc., plus several more useful key-commands. Type \"?\" to\n\
-      \032           Unison to see all available commands.\n\
-      \n\
-      \032  Changes since 2.48:\n\
-      \032    * Repository transplanted from SVN to Git and moved to GitHub ()\n\
-      \032      (https://github.com/bcpierce00/unison).\n\
-      \032    * Add a new preference, 'atomic', for specifying directories that\n\
-      \032      should be treated atomically: if there are changes within such a\n\
-      \032      directory in both replicase, the whole directory is marked as a\n\
-      \032      conflict instead of propagating any of the changes. Thanks to\n\
-      \032      Julian Squires for submitting this patch!\n\
-      \032    * OSX / macOS\n\
-      \032         + Ported to 10.13, High Sierra, and Apple's new APFS (earlier\n\
-      \032           versions of Unison break because of new behavior of\n\
-      \032           AppleDouble files)\n\
-      \032         + Replaced Growl with OS X native notification center.\n\
-      \032    * Miscellaneous:\n\
-      \032         + The OCaml compiler version is now included in the \"connection\n\
-      \032           header -- the string that's printed when connecting to a\n\
-      \032           remote server -- to facilitate debugging version mismatch\n\
-      \032           issues.\n\
-      \032         + Compatible with OCaml 4.06.\n\
-      \032         + Added a DockerFile for the convenience of Docker users.\n\
-      \032         + Many small bugfixes and UI improvements.\n\
-      \n\
-      \032  Changes since 2.45:\n\
-      \032    * Incorporated a patch from Christopher Zimmermann to replace the\n\
-      \032      Uprintf module (which doesn't work with OCaml 4.02, causing Unison\n\
-      \032      to crash) with equivalent functionality from the standard library.\n\
-      \032    * Incorporated a refresh of the OSX GUI, contributed by Alan Shutko.\n\
-      \032    * Added a maxsizethreshold option, which prevents the transfer of\n\
-      \032      files larger than the size specified (in Kb).\n\
-      \032    * Added a \"copyonconflict\" preference, to make a copy of files that\n\
-      \032      would otherwise be overwritten or deleted in case of conflicting\n\
-      \032      changes. (This makes it possible to automatically resolve conflicts\n\
-      \032      in a fairly safe way when synchronizing continuously, in\n\
-      \032      combination with the \"repeat = watch\" and \"prefer = newer\"\n\
-      \032      preferences.\n\
-      \032    * File system monitoring:\n\
-      \032         + The file watcher now fails when unable to watch a directory,\n\
-      \032           rather than silently ignoring the issue.\n\
-      \032         + File system monitoring: more robust communication with the\n\
-      \032           helper program (in socket mode, the unison server will still\n\
-      \032           work properly despite unexpected unison client\n\
-      \032           disconnections).\n\
-      \032         + A bytecode version of unison-fsmonitor is now produced by\n\
-      \032           \"make NATIVE=false\"\n\
-      \032         + Improved search for unison-fsmonitor\n\
-      \032         + Detect when the helper process exits.\n\
-      \032         + More robust file watching helper programs for Windows and\n\
-      \032           Linux. They communicate with Unison through pipes (Unison\n\
-      \032           redirects stdin and stdout), using a race-free protocol.\n\
-      \032         + Retries paths with failures using an exponential backoff\n\
-      \032           algorithm.\n\
-      \032         + The information returned by the file watchers are used\n\
-      \032           independently for each replica; thus, when only one replica\n\
-      \032           has changes, Unison will only rescan this replica.\n\
-      \032         + When available, used by the graphical UIs to speed up\n\
-      \032           rescanning (can be disabled by setting the new watch\n\
-      \032           preference to\n\
-      \032         + Small fix to the way fsmonitor.py gets invoked when using the\n\
-      \032           file watching functionality, suggested by Josh Berdine. Unison\n\
-      \032           will now look for fsmonitor.py in the same directory where the\n\
-      \032           Unison executable itself lives.\n\
-      \032    * Minor:\n\
-      \032         + Fixed a bug in export procedure that was messing up\n\
-      \032           documentation strings.\n\
-      \032         + Incorporated a patch from Ir\225nyossy Knoblauch Art\250r to make\n\
-      \032           temp file names fit within 143 characters (to make eCryptFS\n\
-      \032           happy).\n\
-      \032         + Added a string to the Conflict direction to document the\n\
-      \032           reason of the conflict.\n\
-      \032         + Log conflicts and problems in the text UI even if nothing is\n\
-      \032           propagated.\n\
-      \032         + Use hash function from OCaml 3.x for comparing archives, even\n\
-      \032           when compiled with OCaml 4.x.\n\
-      \032         + Do not restart Unison in case of uncaught exception when the\n\
-      \032           repeat preference is set. This seems safer. And it does not\n\
-      \032           work, for instance, in case of lost connection.\n\
-      \032         + Fix Unix.readlink invalid argument error under Windows\n\
-      \032         + Fix a crash when the output of the diff program is too large.\n\
-      \032         + Fixed Makefile for cross-compiling towards Windows (updated to\n\
-      \032           MinGW-w64)\n\
-      \n\
-      \032  Changes since 2.40.63:\n\
-      \032    * New preference fastercheckUNSAFE, which can be used (with care!) to\n\
-      \032      achieve much faster update detection when all the common files in\n\
-      \032      the two replicas are known to be identical. See the manual for more\n\
-      \032      information.\n\
-      \032      This feature should still be considered experimental, but it's\n\
-      \032      ready for other people to try out.\n\
-      \032    * Added option clientHostName. If specified, it will be used to as\n\
-      \032      the client host name, overriding UNISONLOCALHOSTNAME and the actual\n\
-      \032      host name.\n\
-      \032    * OS X GUI:\n\
-      \032         + fix crash under Lion, because of problems with the toolbar,\n\
-      \032           using the fix suggested in\n\
-      \032           http://blitzbasic.com/Community/posts.php?topic=95778.\n\
-      \032         + uimacnew09 is now the standard graphical interface on OSX\n\
-      \032         + A small improvement to the uimacnew09 interface from Alan\n\
-      \032           Schmitt and Steve Kalkwarf: when Unison is run with the -batch\n\
-      \032           flag, the interface will now automatically propagate changes\n\
-      \032           and terminate, without waiting for user interaction.\n\
-      \032         + Show a modal warning window if there is no archive for the\n\
-      \032           hosts. The user can then choose to exit or proceed (proceed is\n\
-      \032           the default). The window is not shown if the batch preference\n\
-      \032           is true.\n\
-      \032         + file details panel selectable\n\
-      \032    * GTK GUI:\n\
-      \032         + New version of uigtk2.ml from Matt Zagrabelny that reorganizes\n\
-      \032           the icons in a slightly more intuitive way.\n\
-      \032    * Minor fixes:\n\
-      \032         + Setting the prefer preference to older or newer now propagates\n\
-      \032           deletions when there is no conflict.\n\
-      \032         + Correctly quote the path when running merge commands.\n\
-      \032         + Add quotes to paths when calling external file watcher\n\
-      \032           utility.\n\
-      \032         + Incorporate a patch to fsmonitor.py (the external filewatcher\n\
-      \032           utility) from Tomasz Zernicki to make it work better under\n\
-      \032           Windows.\n\
-      \032         + Incorporated new version of fsmonitor.py from Christophe Gohle\n\
-      \032         + Fixed incompatibility with OpenSSH 5.6.\n\
-      \032         + Fixed fingerprint cache: do not cache file properties\n\
-      \032         + Some spelling corrections in documentation and comments from\n\
-      \032           Stephane Glondu\n\
-      \032         + Fixed O_APPEND mode for open under Windows\n\
-      \032         + Fixed String.sub invalid argument error when an AppleDouble\n\
-      \032           file does not contain a finder information field\n\
-      \032         + Trim duplicate paths when using \"-repeat watch\"\n\
-      \032         + Unison now passes path arguments and -follow directives to\n\
-      \032           fsmonitor.py. This seems to work except for one small issue\n\
-      \032           with how fsmonitor.py treats -follow directives for\n\
-      \032           directories that don't exist (or maybe this is an issue with\n\
-      \032           how it treats any kind of monitoring when the thing being\n\
-      \032           monitored doesn't exist?). If we create a symlink to a\n\
-      \032           nonexistent directory, give Unison (hence fsmonitor.py) a\n\
-      \032           'follow' directive for the symlink, start unison, and then\n\
-      \032           create the directory, fsmonitor.py misses the change.\n\
-      \032         + Lines added in profile files by unison always start at a new\n\
-      \032           line\n\
-      \n\
-      \032  Changes since 2.40.1:\n\
-      \032    * Added \"BelowPath\" patterns, that match a path as well as all paths\n\
-      \032      below (convenient to use with nodeletion,update,creationpartial\n\
-      \032      preferences)\n\
-      \032    * Added a \"fat\" preference that makes Unison use the right options\n\
-      \032      when one of the replica is on a FAT filesystem.\n\
-      \032    * Allow \"prefer/force=newer\" even when not synchronizing modification\n\
-      \032      times. (The reconciler will not be aware of the modification time\n\
-      \032      of unchanged files, so the synchronization choices of Unison can be\n\
-      \032      different from when \"times=true\", but the behavior remains sane:\n\
-      \032      changed files with the most recent modification time will be\n\
-      \032      propagated.)\n\
-      \032    * Minor fixes and improvements:\n\
-      \032         + Compare filenames up to decomposition in case sensitive mode\n\
-      \032           when one host is running MacOSX and the unicode preference is\n\
-      \032           set to true.\n\
-      \032         + Rsync: somewhat faster compressor\n\
-      \032         + Make Unicode the default on all architectures (it was only the\n\
-      \032           default when a Mac OS X or Windows machine was involved).\n\
-      \n\
-      \032  Changes since 2.32:\n\
-      \032    * Major enhancement: Unicode support.\n\
-      \032         + Unison should now handle unicode filenames correctly on all\n\
-      \032           platforms.\n\
-      \032         + This functionality is controlled by a new preference unicode.\n\
-      \032         + Unicode mode is now the default when one of the hosts is under\n\
-      \032           Windows or MacOS. This may make upgrades a bit more painful\n\
-      \032           (the archives cannot be reused), but this is a much saner\n\
-      \032           default.\n\
-      \032    * Partial transfer of directories. If an error occurs while\n\
-      \032      transferring a directory, the part transferred so far is copied\n\
-      \032      into place (and the archives are updated accordingly). The\n\
-      \032      \"maxerrors\" preference controls how many transfer error Unison will\n\
-      \032      accept before stopping the transfer of a directory (by default,\n\
-      \032      only one). This makes it possible to transfer most of a directory\n\
-      \032      even if there are some errors. Currently, only the first error is\n\
-      \032      reported by the GUIs.\n\
-      \032      Also, allow partial transfer of a directory when there was an error\n\
-      \032      deep inside this directory during update detection. At the moment,\n\
-      \032      this is only activated with the text and GTK UIs, which have been\n\
-      \032      modified so that they show that the transfer is going to be partial\n\
-      \032      and so that they can display all errors.\n\
-      \032    * Improvement to the code for resuming directory transfers:\n\
-      \032         + if a file was not correctly transferred (or the source has\n\
-      \032           been modified since, with unchanged size), Unison performs a\n\
-      \032           new transfer rather than failing\n\
-      \032         + spurious files are deleted (this can happen if a file is\n\
-      \032           deleted on the source replica before resuming the transfer;\n\
-      \032           not deleting the file would result in it reappearing on the\n\
-      \032           target replica)\n\
-      \032    * Experimental streaming protocol for transferring file contents (can\n\
-      \032      be disabled by setting the directive \"stream\" to false): file\n\
-      \032      contents is transferred asynchronously (without waiting for a\n\
-      \032      response from the destination after each chunk sent) rather than\n\
-      \032      using the synchronous RPC mechanism. As a consequence:\n\
-      \032         + Unison now transfers the contents of a single file at a time\n\
-      \032           (Unison used to transfer several contents simultaneously in\n\
-      \032           order to hide the connection latency.)\n\
-      \032         + the transfer of large files uses the full available bandwidth\n\
-      \032           and is not slowed done due to the connection latency anymore\n\
-      \032         + we get performance improvement for small files as well by\n\
-      \032           scheduling many files simultaneously (as scheduling a file for\n\
-      \032           transfer consume little resource: it does not mean allocating\n\
-      \032           a large buffer anymore)\n\
-      \032    * Changes to the internal implementation of the rsync algorithm:\n\
-      \032         + use longer blocks for large files (the size of a block is the\n\
-      \032           square root of the size of the file for large files);\n\
-      \032         + transmit less checksum information per block (we still have\n\
-      \032           less than one chance in a hundred million of transferring a\n\
-      \032           file incorrectly, and Unison will catch any transfer error\n\
-      \032           when fingerprinting the whole file)\n\
-      \032         + avoid transfer overhead (which was 4 bytes per block)\n\
-      \032      For a 1G file, the first optimization saves a factor 50 on the\n\
-      \032      amount of data transferred from the target to the source (blocks\n\
-      \032      are 32768 bytes rather than just 700 bytes). The two other\n\
-      \032      optimizations save another factor of 2 (from 24 bytes per block\n\
-      \032      down to 10).\n\
-      \032    * Implemented an on-disk file fingerprint cache to speed-up update\n\
-      \032      detection after a crash: this way, Unison does not have do\n\
-      \032      recompute all the file fingerprints from scratch.\n\
-      \032         + When Unison detects that the archive case-sensitivity mode\n\
-      \032           does not match the current settings, it populates the\n\
-      \032           fingerprint cache using the archive contents. This way,\n\
-      \032           changing the case-sensitivity mode should be reasonably fast.\n\
-      \032    * New preferences \"noupdate=root\", \"nodeletion=root\",\n\
-      \032      \"nocreation=root\" that prevent Unison from performing files\n\
-      \032      updates, deletions or creations on the given root. Also 'partial'\n\
-      \032      versions of 'noupdate', 'nodeletion' and 'nocreation'\n\
-      \032    * Limit the number of simultaneous external copy program (\"copymax\"\n\
-      \032      preference)\n\
-      \032    * New \"links\" preference. When set to false, Unison will report an\n\
-      \032      error on symlinks during update detection. (This is the default\n\
-      \032      when one host is running Windows but not Cygwin.) This is better\n\
-      \032      than failing during propagation.\n\
-      \032    * Added a preference \"halfduplex\" to force half-duplex communication\n\
-      \032      with the server. This may be useful on unreliable links (as a more\n\
-      \032      efficient alternative to \"maxthreads = 1\").\n\
-      \032    * Renamed preference \"pretendwin\" to \"ignoreinodenumbers\" (an alias\n\
-      \032      is kept for backwards compatibility).\n\
-      \032    * Ignore one-second differences when synchronizing modification time.\n\
-      \032      (Technically, this is an incompatible archive format change, but it\n\
-      \032      is backward compatible. To trigger a problem, a user would have to\n\
-      \032      synchronize modification times on a filesystem with a two-second\n\
-      \032      granularity and then downgrade to a previous version of Unison,\n\
-      \032      which does not work well in such a case. Thus, it does not seem\n\
-      \032      worthwhile to increment the archive format number, which would\n\
-      \032      impact all users.)\n\
-      \032    * Do not keep many files simultaneously opened anymore when the rsync\n\
-      \032      algorithm is in use.\n\
-      \032    * Add \"ignorearchives\" preference to ignore existing archives (to\n\
-      \032      avoid forcing users to delete them manually, in situations where\n\
-      \032      one archive has gotten deleted or corrupted).\n\
-      \032    * Mac OS\n\
-      \032         + fixed rsync bug which could result in an \"index out of bounds\"\n\
-      \032           error when transferring resource forks.\n\
-      \032         + Fixed bug which made Unison ignore finder information and\n\
-      \032           resource fork when compiled to 64bit on Mac OSX.\n\
-      \032         + should now be 64 bit clean (the Growl framework is not up to\n\
-      \032           date, though)\n\
-      \032         + Made the bridge between Objective C and Ocaml code GC friendly\n\
-      \032           (it was allocating ML values and putting them in an array\n\
-      \032           which was not registered with the GC)\n\
-      \032         + use darker grey arrows (patch contributed by Eric Y. Kow)\n\
-      \032    * GTK user interface\n\
-      \032         + assistant for creating profiles\n\
-      \032         + profile editor\n\
-      \032         + pop up a summary window when the replicas are not fully\n\
-      \032           synchronized after transport\n\
-      \032         + display estimated remaining time and transfer rate on the\n\
-      \032           progress bar\n\
-      \032         + allow simultaneous selection of several items\n\
-      \032         + Do not reload the preference file before a new update\n\
-      \032           detection if it is unchanged\n\
-      \032         + disabled scrolling to the first unfinished item during\n\
-      \032           transport. It goes way too fast when lot of small files are\n\
-      \032           synchronized, and it makes it impossible to browse the file\n\
-      \032           list during transport.\n\
-      \032         + take into account the \"height\" preference again\n\
-      \032         + the internal list of selected reconciler item was not always\n\
-      \032           in sync with what was displayed (GTK bug?); workaround\n\
-      \032           implemented\n\
-      \032         + Do not display \"Looking for change\" messages during\n\
-      \032           propagation (when checking the targe is unchanged) but only\n\
-      \032           during update detection\n\
-      \032         + Apply patch to fix some crashes in the OSX GUI, thanks to Onne\n\
-      \032           Gorter.\n\
-      \032    * Text UI\n\
-      \032         + During update detection, display status by updating a single\n\
-      \032           line rather than generating a new line of output every so\n\
-      \032           often. Should be less confusing.\n\
-      \032    * Windows\n\
-      \032         + Fastcheck is now the default under Windows. People mostly use\n\
-      \032           NTFS nowadays and the Unicode API provides an equivalent to\n\
-      \032           inode numbers for this filesystem.\n\
-      \032         + Only use long UNC path for accessing replicas (as '..' is not\n\
-      \032           handled with this format of paths, but can be useful)\n\
-      \032         + Windows text UI: now put the console into UTF-8 output mode.\n\
-      \032           This is the right thing to do when in Unicode mode, and is no\n\
-      \032           worse than what we had previously otherwise (the console use\n\
-      \032           some esoteric encoding by default). This only works when using\n\
-      \032           a Unicode font instead of the default raster font.\n\
-      \032         + Don't get the home directory from environment variable HOME\n\
-      \032           under Windows (except for Cygwin binaries): we don't want the\n\
-      \032           behavior of Unison to depends on whether it is run from a\n\
-      \032           Cygwin shell (where HOME is set) or in any other way (where\n\
-      \032           HOME is usually not set).\n\
-      \032    * Miscellaneous fixes and improvements\n\
-      \032         + Made a server waiting on a socket more resilient to unexpected\n\
-      \032           lost connections from the client.\n\
-      \032         + Small patch to property setting code suggested by Ulrich\n\
-      \032           Gernkow.\n\
-      \032         + Several fixes to the change transfer functions (both the\n\
-      \032           internal ones and external transfers using rsync). In\n\
-      \032           particular, limit the number of simultaneous transfer using an\n\
-      \032           rsync (as the rsync algorithm can use a large amount of memory\n\
-      \032           when processing huge files)\n\
-      \032         + Keep track of which file contents are being transferred, and\n\
-      \032           delay the transfer of a file when another file with the same\n\
-      \032           contents is currently being transferred. This way, the second\n\
-      \032           transfer can be skipped and replaced by a local copy.\n\
-      \032         + Experimental update detection optimization: do not read the\n\
-      \032           contents of unchanged directories\n\
-      \032         + When a file transfer fails, turn off fastcheck for this file\n\
-      \032           on the next sync.\n\
-      \032         + Fixed bug with case insensitive mode on a case sensitive\n\
-      \032           filesystem:\n\
-      \032              o if file \"a/a\" is created on one replica and directory \"A\"\n\
-      \032                is created on the other, the file failed to be\n\
-      \032                synchronized the first time Unison is run afterwards, as\n\
-      \032                Unison uses the wrong path \"a/a\" (if Unison is run again,\n\
-      \032                the directories are in the archive, so the right path is\n\
-      \032                used);\n\
-      \032              o if file \"a\" appears on one replica and file \"A\" appears\n\
-      \032                on the other with different contents, Unison was unable\n\
-      \032                to synchronize them.\n\
-      \032         + Improved error reporting when the destination is updated\n\
-      \032           during synchronization: Unison now tells which file has been\n\
-      \032           updated, and how.\n\
-      \032         + Limit the length of temporary file names\n\
-      \032         + Case sensitivity information put in the archive (in a backward\n\
-      \032           compatible way) and checked when the archive is loaded\n\
-      \032         + Got rid of the 16mb marshalling limit by marshalling to a\n\
-      \032           bigarray.\n\
-      \032         + Resume copy of partially transferred files.\n\
-      \n\
-      \032  Changes since 2.31:\n\
-      \032    * Small user interface changes\n\
-      \032         + Small change to text UI \"scanning...\" messages, to print just\n\
-      \032           directories (hopefully making it clearer that individual files\n\
-      \032           are not necessarily being fingerprinted).\n\
-      \032    * Minor fixes and improvements:\n\
-      \032         + Ignore one hour differences when deciding whether a file may\n\
-      \032           have been updated. This avoids slow update detection after\n\
-      \032           daylight saving time changes under Windows. This makes Unison\n\
-      \032           slightly more likely to miss an update, but it should be safe\n\
-      \032           enough.\n\
-      \032         + Fix a small bug that was affecting mainly windows users. We\n\
-      \032           need to commit the archives at the end of the sync even if\n\
-      \032           there are no updates to propagate because some files (in fact,\n\
-      \032           if we've just switched to DST on windows, a LOT of files)\n\
-      \032           might have new modtimes in the archive. (Changed the text UI\n\
-      \032           only. It's less clear where to change the GUI.)\n\
-      \032         + Don't delete the temp file when a transfer fails due to a\n\
-      \032           fingerprint mismatch (so that we can have a look and see why!)\n\
-      \032           We've also added more debugging code togive more informative\n\
-      \032           error messages when we encounter the dreaded and longstanding\n\
-      \032           \"assert failed during file transfer\" bug\n\
-      \032         + Incorrect paths (\"path\" directive) now result in an error\n\
-      \032           update item rather than a fatal error.\n\
-      \032         + Create parent directories (with correct permissions) during\n\
-      \032           transport for paths which point to non-existent locations in\n\
-      \032           the destination replica.\n\
-      \n\
-      \032  Changes since 2.27:\n\
-      \032    * If Unison is interrupted during a directory transfer, it will now\n\
-      \032      leave the partially transferred directory intact in a temporary\n\
-      \032      location. (This maintains the invariant that new files/directories\n\
-      \032      are transferred either completely or not at all.) The next time\n\
-      \032      Unison is run, it will continue filling in this temporary\n\
-      \032      directory, skipping transferring files that it finds are already\n\
-      \032      there.\n\
-      \032    * We've added experimental support for invoking an external file\n\
-      \032      transfer tool for whole-file copies instead of Unison's built-in\n\
-      \032      transfer protocol. Three new preferences have been added:\n\
-      \032         + copyprog is a string giving the name (and command-line\n\
-      \032           switches, if needed) of an external program that can be used\n\
-      \032           to copy large files efficiently. By default, rsync is invoked,\n\
-      \032           but other tools such as scp can be used instead by changing\n\
-      \032           the value of this preference. (Although this is not its\n\
-      \032           primary purpose, rsync is actually a pretty fast way of\n\
-      \032           copying files that don't already exist on the receiving host.)\n\
-      \032           For files that do already exist on (but that have been changed\n\
-      \032           in one replica), Unison will always use its built-in\n\
-      \032           implementation of the rsync algorithm.\n\
-      \032         + Added a \"copyprogrest\" preference, so that we can give\n\
-      \032           different command lines for invoking the external copy utility\n\
-      \032           depending on whether a partially transferred file already\n\
-      \032           exists or not. (Rsync doesn't seem to care about this, but\n\
-      \032           other utilities may.)\n\
-      \032         + copythreshold is an integer (-1 by default), indicating above\n\
-      \032           what filesize (in megabytes) Unison should use the external\n\
-      \032           copying utility specified by copyprog. Specifying 0 will cause\n\
-      \032           ALL copies to use the external program; a negative number will\n\
-      \032           prevent any files from using it. (Default is -1.)\n\
-      \032      Thanks to Alan Schmitt for a huge amount of hacking and to an\n\
-      \032      anonymous sponsor for suggesting and underwriting this extension.\n\
-      \032    * Small improvements:\n\
-      \032         + Added a new preference, dontchmod. By default, Unison uses the\n\
-      \032           chmod system call to set the permission bits of files after it\n\
-      \032           has copied them. But in some circumstances (and under some\n\
-      \032           operating systems), the chmod call always fails. Setting this\n\
-      \032           preference completely prevents Unison from ever calling chmod.\n\
-      \032         + Don't ignore files that look like backup files if the\n\
-      \032           backuplocation preference is set to central\n\
-      \032         + Shortened the names of several preferences. The old names are\n\
-      \032           also still supported, for backwards compatibility, but they do\n\
-      \032           not appear in the documentation.\n\
-      \032         + Lots of little documentation tidying. (In particular,\n\
-      \032           preferences are separated into Basic and Advanced! This should\n\
-      \032           hopefully make Unison a little more approachable for new\n\
-      \032           users.\n\
-      \032         + Unison can sometimes fail to transfer a file, giving the\n\
-      \032           unhelpful message \"Destination updated during synchronization\"\n\
-      \032           even though the file has not been changed. This can be caused\n\
-      \032           by programs that change either the file's contents or the\n\
-      \032           file's extended attributes without changing its modification\n\
-      \032           time. It's not clear what is the best fix for this - it is not\n\
-      \032           Unison's fault, but it makes Unison's behavior puzzling - but\n\
-      \032           at least Unison can be more helpful about suggesting a\n\
-      \032           workaround (running once with fastcheck set to false). The\n\
-      \032           failure message has been changed to give this advice.\n\
-      \032         + Further improvements to the OS X GUI (thanks to Alan Schmitt\n\
-      \032           and Craig Federighi).\n\
-      \032    * Very preliminary support for triggering Unison from an external\n\
-      \032      filesystem-watching utility. The current implementation is very\n\
-      \032      simple, not efficient, and almost completely untested--not ready\n\
-      \032      for real users. But if someone wants to help improve it (e.g., by\n\
-      \032      writing a filesystem watcher for your favorite OS), please make\n\
-      \032      yourself known!\n\
-      \032      On the Unison side, the new behavior is very simple:\n\
-      \032         + use the text UI\n\
-      \032         + start Unison with the command-line flag \"-repeat FOO\", where\n\
-      \032           FOO is name of a file where Unison should look for\n\
-      \032           notifications of changes\n\
-      \032         + when it starts up, Unison will read the whole contents of this\n\
-      \032           file (on both hosts), which should be a newline-separated list\n\
-      \032           of paths (relative to the root of the synchronization) and\n\
-      \032           synchronize just these paths, as if it had been started with\n\
-      \032           the \"-path=xxx\" option for each one of them\n\
-      \032         + when it finishes, it will sleep for a few seconds and then\n\
-      \032           examine the watchfile again; if anything has been added, it\n\
-      \032           will read the new paths, synchronize them, and go back to\n\
-      \032           sleep\n\
-      \032         + that's it!\n\
-      \032      To use this to drive Unison \"incrementally,\" just start it in this\n\
-      \032      mode and start up a tool (on each host) to watch for new changes to\n\
-      \032      the filesystem and append the appropriate paths to the watchfile.\n\
-      \032      Hopefully such tools should not be too hard to write.\n\
-      \032    * Bug fixes:\n\
-      \032         + Fixed a bug that was causing new files to be created with\n\
-      \032           permissions 0x600 instead of using a reasonable default (like\n\
-      \032           0x644), if the 'perms' flag was set to 0. (Bug reported by Ben\n\
-      \032           Crowell.)\n\
-      \032         + Follow maxthreads preference when transferring directories.\n\
-      \n\
-      \032  Changes since 2.17:\n\
-      \032    * Major rewrite and cleanup of the whole Mac OS X graphical user\n\
-      \032      interface by Craig Federighi. Thanks, Craig!!!\n\
-      \032    * Small fix to ctime (non-)handling in update detection under windows\n\
-      \032      with fastcheck.\n\
-      \032    * Several small fixes to the GTK2 UI to make it work better under\n\
-      \032      Windows [thanks to Karl M for these].\n\
-      \032    * The backup functionality has been completely rewritten. The\n\
-      \032      external interface has not changed, but numerous bugs, irregular\n\
-      \032      behaviors, and cross-platform inconsistencies have been corrected.\n\
-      \032    * The Unison project now accepts donations via PayPal. If you'd like\n\
-      \032      to donate, you can find a link to the donation page on the Unison\n\
-      \032      home page (http://www.cis.upenn.edu/ bcpierce/unison/lists.html).\n\
-      \032    * Some important safety improvements:\n\
-      \032         + Added a new mountpoint preference, which can be used to\n\
-      \032           specify a path that must exist in both replicas at the end of\n\
-      \032           update detection (otherwise Unison aborts). This can be used\n\
-      \032           to avoid potentially dangerous situations when Unison is used\n\
-      \032           with removable media such as external hard drives and compact\n\
-      \032           flash cards.\n\
-      \032         + The confirmation of \"big deletes\" is now controlled by a\n\
-      \032           boolean preference confirmbigdeletes. Default is true, which\n\
-      \032           gives the same behavior as previously. (This functionality is\n\
-      \032           at least partly superseded by the mountpoint preference, but\n\
-      \032           it has been left in place in case it is useful to some\n\
-      \032           people.)\n\
-      \032         + If Unison is asked to \"follow\" a symbolic link but there is\n\
-      \032           nothing at the other end of the link, it will now flag this\n\
-      \032           path as an error, rather than treating the symlink itself as\n\
-      \032           missing or deleted. This avoids a potentially dangerous\n\
-      \032           situation where a followed symlink points to an external\n\
-      \032           filesystem that might be offline when Unison is run (whereupon\n\
-      \032           Unison would cheerfully delete the corresponding files in the\n\
-      \032           other replica!).\n\
-      \032    * Smaller changes:\n\
-      \032         + Added forcepartial and preferpartial preferences, which behave\n\
-      \032           like force and prefer but can be specified on a per-path\n\
-      \032           basis. [Thanks to Alan Schmitt for this.]\n\
-      \032         + A bare-bones self test feature was added, which runs unison\n\
-      \032           through some of its paces and checks that the results are as\n\
-      \032           expected. The coverage of the tests is still very limited, but\n\
-      \032           the facility has already been very useful in debugging the new\n\
-      \032           backup functionality (especially in exposing some subtle\n\
-      \032           cross-platform issues).\n\
-      \032         + Refined debugging code so that the verbosity of individual\n\
-      \032           modules can be controlled separately. Instead of just putting\n\
-      \032           '-debug verbose' on the command line, you can put '-debug\n\
-      \032           update+', which causes all the extra messages in the Update\n\
-      \032           module, but not other modules, to be printed. Putting '-debug\n\
-      \032           verbose' causes all modules to print with maximum verbosity.\n\
-      \032         + Removed mergebatch preference. (It never seemed very useful,\n\
-      \032           and its semantics were confusing.)\n\
-      \032         + Rewrote some of the merging functionality, for better\n\
-      \032           cooperation with external Harmony instances.\n\
-      \032         + Changed the temp file prefix from .# to .unison.\n\
-      \032         + Compressed the output from the text user interface\n\
-      \032           (particularly when run with the -terse flag) to make it easier\n\
-      \032           to interpret the results when Unison is run several times in\n\
-      \032           succession from a script.\n\
-      \032         + Diff and merge functions now work under Windows.\n\
-      \032         + Changed the order of arguments to the default diff command (so\n\
-      \032           that the + and - annotations in diff's output are reversed).\n\
-      \032         + Added .mpp files to the \"never fastcheck\" list (like .xls\n\
-      \032           files).\n\
-      \032    * Many small bugfixes, including:\n\
-      \032         + Fixed a longstanding bug regarding fastcheck and daylight\n\
-      \032           saving time under Windows when Unison is set up to synchronize\n\
-      \032           modification times. (Modification times cannot be updated in\n\
-      \032           the archive in this case, so we have to ignore one hour\n\
-      \032           differences.)\n\
-      \032         + Fixed a bug that would occasionally cause the archives to be\n\
-      \032           left in non-identical states on the two hosts after\n\
-      \032           synchronization.\n\
-      \032         + Fixed a bug that prevented Unison from communicating correctly\n\
-      \032           between 32- and 64-bit architectures.\n\
-      \032         + On windows, file creation times are no longer used as a proxy\n\
-      \032           for inode numbers. (This is unfortunate, as it makes fastcheck\n\
-      \032           a little less safe. But it turns out that file creation times\n\
-      \032           are not reliable under Windows: if a file is removed and a new\n\
-      \032           file is created in its place, the new one will sometimes be\n\
-      \032           given the same creation date as the old one!)\n\
-      \032         + Set read-only file to R/W on OSX before attempting to change\n\
-      \032           other attributes.\n\
-      \032         + Fixed bug resulting in spurious \"Aborted\" errors during\n\
-      \032           transport (thanks to Jerome Vouillon)\n\
-      \032         + Enable diff if file contents have changed in one replica, but\n\
-      \032           only properties in the other.\n\
-      \032         + Removed misleading documentation for 'repeat' preference.\n\
-      \032         + Fixed a bug in merging code where Unison could sometimes\n\
-      \032           deadlock with the external merge program, if the latter\n\
-      \032           produced large amounts of output.\n\
-      \032         + Workaround for a bug compiling gtk2 user interface against\n\
-      \032           current versions of gtk2+ libraries.\n\
-      \032         + Added a better error message for \"ambiguous paths\".\n\
-      \032         + Squashed a longstanding bug that would cause file transfer to\n\
-      \032           fail with the message \"Failed: Error in readWrite: Is a\n\
-      \032           directory.\"\n\
-      \032         + Replaced symlinks with copies of their targets in the Growl\n\
-      \032           framework in src/uimac. This should make the sources easier to\n\
-      \032           check out from the svn repository on WinXP systems.\n\
-      \032         + Added a workaround (suggested by Karl M.) for the problem\n\
-      \032           discussed on the unison users mailing list where, on the\n\
-      \032           Windows platform, the server would hang when transferring\n\
-      \032           files. I conjecture that the problem has to do with the RPC\n\
-      \032           mechanism, which was used to make a call back from the server\n\
-      \032           to the client (inside the Trace.log function) so that the log\n\
-      \032           message would be appended to the log file on the client. The\n\
-      \032           workaround is to dump these messages (about when xferbycopying\n\
-      \032           shortcuts are applied and whether they succeed) just to the\n\
-      \032           standard output of the Unison process, not to the log file.\n\
-      \n\
-      \032  Changes since 2.13.0:\n\
-      \032    * The features for performing backups and for invoking external merge\n\
-      \032      programs have been completely rewritten by Stephane Lescuyer\n\
-      \032      (thanks, Stephane!). The user-visible functionality should not\n\
-      \032      change, but the internals have been rationalized and there are a\n\
-      \032      number of new features. See the manual (in particular, the\n\
-      \032      description of the backupXXX preferences) for details.\n\
-      \032    * Incorporated patches for ipv6 support, contributed by Samuel\n\
-      \032      Thibault. (Note that, due to a bug in the released OCaml 3.08.3\n\
-      \032      compiler, this code will not actually work with ipv6 unless\n\
-      \032      compiled with the CVS version of the OCaml compiler, where the bug\n\
-      \032      has been fixed; however, ipv4 should continue to work normally.)\n\
-      \032    * OSX interface:\n\
-      \032         + Incorporated Ben Willmore's cool new icon for the Mac UI.\n\
-      \032    * Small fixes:\n\
-      \032         + Fixed off by one error in month numbers (in printed dates)\n\
-      \032           reported by Bob Burger\n\
-      \n\
-      \032  Changes since 2.12.0:\n\
-      \032    * New convention for release numbering: Releases will continue to be\n\
-      \032      given numbers of the form X.Y.Z, but, from now on, just the major\n\
-      \032      version number (X.Y) will be considered significant when checking\n\
-      \032      compatibility between client and server versions. The third\n\
-      \032      component of the version number will be used only to identify\n\
-      \032      \"patch levels\" of releases.\n\
-      \032      This change goes hand in hand with a change to the procedure for\n\
-      \032      making new releases. Candidate releases will initially be given\n\
-      \032      \"beta release\" status when they are announced for public\n\
-      \032      consumption. Any bugs that are discovered will be fixed in a\n\
-      \032      separate branch of the source repository (without changing the\n\
-      \032      major version number) and new tarballs re-released as needed. When\n\
-      \032      this process converges, the patched beta version will be dubbed\n\
-      \032      stable.\n\
-      \032    * Warning (failure in batch mode) when one path is completely\n\
-      \032      emptied. This prevents Unison from deleting everything on one\n\
-      \032      replica when the other disappear.\n\
-      \032    * Fix diff bug (where no difference is shown the first time the diff\n\
-      \032      command is given).\n\
-      \032    * User interface changes:\n\
-      \032         + Improved workaround for button focus problem (GTK2 UI)\n\
-      \032         + Put leading zeroes in date fields\n\
-      \032         + More robust handling of character encodings in GTK2 UI\n\
-      \032         + Changed format of modification time displays, from modified at\n\
-      \032           hh:mm:ss on dd MMM, yyyy to modified on yyyy-mm-dd hh:mm:ss\n\
-      \032         + Changed time display to include seconds (so that people on FAT\n\
-      \032           filesystems will not be confused when Unison tries to update a\n\
-      \032           file time to an odd number of seconds and the filesystem\n\
-      \032           truncates it to an even number!)\n\
-      \032         + Use the diff \"-u\" option by default when showing differences\n\
-      \032           between files (the output is more readable)\n\
-      \032         + In text mode, pipe the diff output to a pager if the\n\
-      \032           environment variable PAGER is set\n\
-      \032         + Bug fixes and cleanups in ssh password prompting. Now works\n\
-      \032           with the GTK2 UI under Linux. (Hopefully the Mac OS X one is\n\
-      \032           not broken!)\n\
-      \032         + Include profile name in the GTK2 window name\n\
-      \032         + Added bindings ',' (same as '<') and '.' (same as '>') in the\n\
-      \032           GTK2 UI\n\
-      \032    * Mac GUI:\n\
-      \032         + actions like < and > scroll to the next item as necessary.\n\
-      \032         + Restart has a menu item and keyboard shortcut (command-R).\n\
-      \032         + Added a command-line tool for Mac OS X. It can be installed\n\
-      \032           from the Unison menu.\n\
-      \032         + New icon.\n\
-      \032         + Handle the \"help\" command-line argument properly.\n\
-      \032         + Handle profiles given on the command line properly.\n\
-      \032         + When a profile has been selected, the profile dialog is\n\
-      \032           replaced by a \"connecting\" message while the connection is\n\
-      \032           being made. This gives better feedback.\n\
-      \032         + Size of left and right columns is now large enough so that\n\
-      \032           \"PropsChanged\" is not cut off.\n\
-      \032    * Minor changes:\n\
-      \032         + Disable multi-threading when both roots are local\n\
-      \032         + Improved error handling code. In particular, make sure all\n\
-      \032           files are closed in case of a transient failure\n\
-      \032         + Under Windows, use $UNISON for home directory as a last resort\n\
-      \032           (it was wrongly moved before $HOME and $USERPROFILE in Unison\n\
-      \032           2.12.0)\n\
-      \032         + Reopen the logfile if its name changes (profile change)\n\
-      \032         + Double-check that permissions and modification times have been\n\
-      \032           properly set: there are some combination of OS and filesystem\n\
-      \032           on which setting them can fail in a silent way.\n\
-      \032         + Check for bad Windows filenames for pure Windows\n\
-      \032           synchronization also (not just cross architecture\n\
-      \032           synchronization). This way, filenames containing backslashes,\n\
-      \032           which are not correctly handled by unison, are rejected right\n\
-      \032           away.\n\
-      \032         + Attempt to resolve issues with synchronizing modification\n\
-      \032           times of read-only files under Windows\n\
-      \032         + Ignore chmod failures when deleting files\n\
-      \032         + Ignore trailing dots in filenames in case insensitive mode\n\
-      \032         + Proper quoting of paths, files and extensions ignored using\n\
-      \032           the UI\n\
-      \032         + The strings CURRENT1 and CURRENT2 are now correctly substitued\n\
-      \032           when they occur in the diff preference\n\
-      \032         + Improvements to syncing resource forks between Macs via a\n\
-      \032           non-Mac system.\n\
-      \n\
-      \032  Changes since 2.10.2:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed.\n\
-      \032    * Source code availability: The Unison sources are now managed using\n\
-      \032      Subversion. One nice side-effect is that anonymous checkout is now\n\
-      \032      possible, like this:\n\
-      \032       svn co https://cvs.cis.upenn.edu:3690/svnroot/unison/\n\
-      \n\
-      \032      We will also continue to export a \"developer tarball\" of the\n\
-      \032      current (modulo one day) sources in the web export directory. To\n\
-      \032      receive commit logs for changes to the sources, subscribe to the\n\
-      \032      unison-hackers list (http://www.cis.upenn.edu/\n\
-      \032      bcpierce/unison/lists.html).\n\
-      \032    * Text user interface:\n\
-      \032         + Substantial reworking of the internal logic of the text UI to\n\
-      \032           make it a bit easier to modify.\n\
-      \032         + The dumbtty flag in the text UI is automatically set to true\n\
-      \032           if the client is running on a Unix system and the EMACS\n\
-      \032           environment variable is set to anything other than the empty\n\
-      \032           string.\n\
-      \032    * Native OS X gui:\n\
-      \032         + Added a synchronize menu item with keyboard shortcut\n\
-      \032         + Added a merge menu item, still needs to be debugged\n\
-      \032         + Fixes to compile for Panther\n\
-      \032         + Miscellaneous improvements and bugfixes\n\
-      \032    * Small changes:\n\
-      \032         + Changed the filename checking code to apply to Windows only,\n\
-      \032           instead of OS X as well.\n\
-      \032         + Finder flags now synchronized\n\
-      \032         + Fallback in copy.ml for filesystem that do not support O_EXCL\n\
-      \032         + Changed buffer size for local file copy (was highly\n\
-      \032           inefficient with synchronous writes)\n\
-      \032         + Ignore chmod failure when deleting a directory\n\
-      \032         + Fixed assertion failure when resolving a conflict content\n\
-      \032           change / permission changes in favor of the content change.\n\
-      \032         + Workaround for transferring large files using rsync.\n\
-      \032         + Use buffered I/O for files (this is the only way to open files\n\
-      \032           in binary mode under Cygwin).\n\
-      \032         + On non-Cygwin Windows systems, the UNISON environment variable\n\
-      \032           is now checked first to determine where to look for Unison's\n\
-      \032           archive and preference files, followed by HOME and USERPROFILE\n\
-      \032           in that order. On Unix and Cygwin systems, HOME is used.\n\
-      \032         + Generalized diff preference so that it can be given either as\n\
-      \032           just the command name to be used for calculating diffs or else\n\
-      \032           a whole command line, containing the strings CURRENT1 and\n\
-      \032           CURRENT2, which will be replaced by the names of the files to\n\
-      \032           be diff'ed before the command is called.\n\
-      \032         + Recognize password prompts in some newer versions of ssh.\n\
-      \n\
-      \032  Changes since 2.9.20:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed.\n\
-      \032    * Major functionality changes:\n\
-      \032         + Major tidying and enhancement of 'merge' functionality. The\n\
-      \032           main user-visible change is that the external merge program\n\
-      \032           may either write the merged output to a single new file, as\n\
-      \032           before, or it may modify one or both of its input files, or it\n\
-      \032           may write two new files. In the latter cases, its\n\
-      \032           modifications will be copied back into place on both the local\n\
-      \032           and the remote host, and (if the two files are now equal) the\n\
-      \032           archive will be updated appropriately. More information can be\n\
-      \032           found in the user manual. Thanks to Malo Denielou and Alan\n\
-      \032           Schmitt for these improvements.\n\
-      \032           Warning: the new merging functionality is not completely\n\
-      \032           compatible with old versions! Check the manual for details.\n\
-      \032         + Files larger than 2Gb are now supported.\n\
-      \032         + Added preliminary (and still somewhat experimental) support\n\
-      \032           for the Apple OS X operating system.\n\
-      \032              o Resource forks should be transferred correctly. (See the\n\
-      \032                manual for details of how this works when synchronizing\n\
-      \032                HFS with non-HFS volumes.) Synchronization of file type\n\
-      \032                and creator information is also supported.\n\
-      \032              o On OSX systems, the name of the directory for storing\n\
-      \032                Unison's archives, preference files, etc., is now\n\
-      \032                determined as follows:\n\
-      \032                   # if ~/.unison exists, use it\n\
-      \032                   # otherwise, use ~/Library/Application Support/Unison,\n\
-      \032                     creating it if necessary.\n\
-      \032              o A preliminary native-Cocoa user interface is under\n\
-      \032                construction. This still needs some work, and some users\n\
-      \032                experience unpredictable crashes, so it is only for\n\
-      \032                hackers for now. Run make with UISTYLE=mac to build this\n\
-      \032                interface.\n\
-      \032    * Minor functionality changes:\n\
-      \032         + Added an ignorelocks preference, which forces Unison to\n\
-      \032           override left-over archive locks. (Setting this preference is\n\
-      \032           dangerous! Use it only if you are positive you know what you\n\
-      \032           are doing.)\n\
-      \032         + Added a new preference assumeContentsAreImmutable. If a\n\
-      \032           directory matches one of the patterns set in this preference,\n\
-      \032           then update detection is skipped for files in this directory.\n\
-      \032           (The purpose is to speed update detection for cases like Mail\n\
-      \032           folders, which contain lots and lots of immutable files.) Also\n\
-      \032           a preference assumeContentsAreImmutableNot, which overrides\n\
-      \032           the first, similarly to ignorenot. (Later amendment: these\n\
-      \032           preferences are now called immutable and immutablenot.)\n\
-      \032         + The ignorecase flag has been changed from a boolean to a\n\
-      \032           three-valued preference. The default setting, called default,\n\
-      \032           checks the operating systems running on the client and server\n\
-      \032           and ignores filename case if either of them is OSX or Windows.\n\
-      \032           Setting ignorecase to true or false overrides this behavior.\n\
-      \032           If you have been setting ignorecase on the command line using\n\
-      \032           -ignorecase=true or -ignorecase=false, you will need to change\n\
-      \032           to -ignorecase true or -ignorecase false.\n\
-      \032         + a new preference, 'repeat', for the text user interface\n\
-      \032           (only). If 'repeat' is set to a number, then, after it\n\
-      \032           finishes synchronizing, Unison will wait for that many seconds\n\
-      \032           and then start over, continuing this way until it is killed\n\
-      \032           from outside. Setting repeat to true will automatically set\n\
-      \032           the batch preference to true.\n\
-      \032         + Excel files are now handled specially, so that the fastcheck\n\
-      \032           optimization is skipped even if the fastcheck flag is set.\n\
-      \032           (Excel does some naughty things with modtimes, making this\n\
-      \032           optimization unreliable and leading to failures during change\n\
-      \032           propagation.)\n\
-      \032         + The ignorecase flag has been changed from a boolean to a\n\
-      \032           three-valued preference. The default setting, called\n\
-      \032           'default', checks the operating systems running on the client\n\
-      \032           and server and ignores filename case if either of them is OSX\n\
-      \032           or Windows. Setting ignorecase to 'true' or 'false' overrides\n\
-      \032           this behavior.\n\
-      \032         + Added a new preference, 'repeat', for the text user interface\n\
-      \032           (only, at the moment). If 'repeat' is set to a number, then,\n\
-      \032           after it finishes synchronizing, Unison will wait for that\n\
-      \032           many seconds and then start over, continuing this way until it\n\
-      \032           is killed from outside. Setting repeat to true will\n\
-      \032           automatically set the batch preference to true.\n\
-      \032         + The 'rshargs' preference has been split into 'rshargs' and\n\
-      \032           'sshargs' (mainly to make the documentation clearer). In fact,\n\
-      \032           'rshargs' is no longer mentioned in the documentation at all,\n\
-      \032           since pretty much everybody uses ssh now anyway.\n\
-      \032    * Documentation\n\
-      \032         + The web pages have been completely redesigned and reorganized.\n\
-      \032           (Thanks to Alan Schmitt for help with this.)\n\
-      \032    * User interface improvements\n\
-      \032         + Added a GTK2 user interface, capable (among other things) of\n\
-      \032           displaying filenames in any locale encoding. Kudos to Stephen\n\
-      \032           Tse for contributing this code!\n\
-      \032         + The text UI now prints a list of failed and skipped transfers\n\
-      \032           at the end of synchronization.\n\
-      \032         + Restarting update detection from the graphical UI will reload\n\
-      \032           the current profile (which in particular will reset the -path\n\
-      \032           preference, in case it has been narrowed by using the \"Recheck\n\
-      \032           unsynchronized items\" command).\n\
-      \032         + Several small improvements to the text user interface,\n\
-      \032           including a progress display.\n\
-      \032    * Bug fixes (too numerous to count, actually, but here are some):\n\
-      \032         + The maxthreads preference works now.\n\
-      \032         + Fixed bug where warning message about uname returning an\n\
-      \032           unrecognized result was preventing connection to server. (The\n\
-      \032           warning is no longer printed, and all systems where 'uname'\n\
-      \032           returns anything other than 'Darwin' are assumed not to be\n\
-      \032           running OS X.)\n\
-      \032         + Fixed a problem on OS X that caused some valid file names\n\
-      \032           (e.g., those including colons) to be considered invalid.\n\
-      \032         + Patched Path.followLink to follow links under cygwin in\n\
-      \032           addition to Unix (suggested by Matt Swift).\n\
-      \032         + Small change to the storeRootsName function, suggested by\n\
-      \032           bliviero at ichips.intel.com, to fix a problem in unison with\n\
-      \032           the `rootalias' option, which allows you to tell unison that\n\
-      \032           two roots contain the same files. Rootalias was being applied\n\
-      \032           after the hosts were sorted, so it wouldn't work properly in\n\
-      \032           all cases.\n\
-      \032         + Incorporated a fix by Dmitry Bely for setting utimes of\n\
-      \032           read-only files on Win32 systems.\n\
-      \032    * Installation / portability:\n\
-      \032         + Unison now compiles with OCaml version 3.07 and later out of\n\
-      \032           the box.\n\
-      \032         + Makefile.OCaml fixed to compile out of the box under OpenBSD.\n\
-      \032         + a few additional ports (e.g. OpenBSD, Zaurus/IPAQ) are now\n\
-      \032           mentioned in the documentation\n\
-      \032         + Unison can now be installed easily on OSX systems using the\n\
-      \032           Fink package manager\n\
-      \n\
-      \032  Changes since 2.9.1:\n\
-      \032    * Added a preference maxthreads that can be used to limit the number\n\
-      \032      of simultaneous file transfers.\n\
-      \032    * Added a backupdir preference, which controls where backup files are\n\
-      \032      stored.\n\
-      \032    * Basic support added for OSX. In particular, Unison now recognizes\n\
-      \032      when one of the hosts being synchronized is running OSX and\n\
-      \032      switches to a case-insensitive treatment of filenames (i.e., 'foo'\n\
-      \032      and 'FOO' are considered to be the same file). (OSX is not yet\n\
-      \032      fully working, however: in particular, files with resource forks\n\
-      \032      will not be synchronized correctly.)\n\
-      \032    * The same hash used to form the archive name is now also added to\n\
-      \032      the names of the temp files created during file transfer. The\n\
-      \032      reason for this is that, during update detection, we are going to\n\
-      \032      silently delete any old temp files that we find along the way, and\n\
-      \032      we want to prevent ourselves from deleting temp files belonging to\n\
-      \032      other instances of Unison that may be running in parallel, e.g.\n\
-      \032      synchronizing with a different host. Thanks to Ruslan Ermilov for\n\
-      \032      this suggestion.\n\
-      \032    * Several small user interface improvements\n\
-      \032    * Documentation\n\
-      \032         + FAQ and bug reporting instructions have been split out as\n\
-      \032           separate HTML pages, accessible directly from the unison web\n\
-      \032           page.\n\
-      \032         + Additions to FAQ, in particular suggestions about performance\n\
-      \032           tuning.\n\
-      \032    * Makefile\n\
-      \032         + Makefile.OCaml now sets UISTYLE=text or UISTYLE=gtk\n\
-      \032           automatically, depending on whether it finds lablgtk installed\n\
-      \032         + Unison should now compile \"out of the box\" under OSX\n\
-      \n\
-      \032  Changes since 2.8.1:\n\
-      \032    * Changing profile works again under Windows\n\
-      \032    * File movement optimization: Unison now tries to use local copy\n\
-      \032      instead of transfer for moved or copied files. It is controlled by\n\
-      \032      a boolean option \"xferbycopying\".\n\
-      \032    * Network statistics window (transfer rate, amount of data\n\
-      \032      transferred). [NB: not available in Windows-Cygwin version.]\n\
-      \032    * symlinks work under the cygwin version (which is dynamically\n\
-      \032      linked).\n\
-      \032    * Fixed potential deadlock when synchronizing between Windows and\n\
-      \032      Unix\n\
-      \032    * Small improvements:\n\
-      \032         + If neither the USERPROFILE nor the HOME environment variables\n\
-      \032           are set, then Unison will put its temporary commit log (called\n\
-      \032           DANGER.README) into the directory named by the UNISON\n\
-      \032           environment variable, if any; otherwise it will use C:.\n\
-      \032         + alternative set of values for fastcheck: yes = true; no =\n\
-      \032           false; default = auto.\n\
-      \032         + -silent implies -contactquietly\n\
-      \032    * Source code:\n\
-      \032         + Code reorganization and tidying. (Started breaking up some of\n\
-      \032           the basic utility modules so that the non-unison-specific\n\
-      \032           stuff can be made available for other projects.)\n\
-      \032         + several Makefile and docs changes (for release);\n\
-      \032         + further comments in \"update.ml\";\n\
-      \032         + connection information is not stored in global variables\n\
-      \032           anymore.\n\
-      \n\
-      \032  Changes since 2.7.78:\n\
-      \032    * Small bugfix to textual user interface under Unix (to avoid leaving\n\
-      \032      the terminal in a bad state where it would not echo inputs after\n\
-      \032      Unison exited).\n\
-      \n\
-      \032  Changes since 2.7.39:\n\
-      \032    * Improvements to the main web page (stable and beta version docs are\n\
-      \032      now both accessible).\n\
-      \032    * User manual revised.\n\
-      \032    * Added some new preferences:\n\
-      \032         + \"sshcmd\" and \"rshcmd\" for specifying paths to ssh and rsh\n\
-      \032           programs.\n\
-      \032         + \"contactquietly\" for suppressing the \"contacting server\"\n\
-      \032           message during Unison startup (under the graphical UI).\n\
-      \032    * Bug fixes:\n\
-      \032         + Fixed small bug in UI that neglected to change the displayed\n\
-      \032           column headers if loading a new profile caused the roots to\n\
-      \032           change.\n\
-      \032         + Fixed a bug that would put the text UI into an infinite loop\n\
-      \032           if it encountered a conflict when run in batch mode.\n\
-      \032         + Added some code to try to fix the display of non-Ascii\n\
-      \032           characters in filenames on Windows systems in the GTK UI.\n\
-      \032           (This code is currently untested--if you're one of the people\n\
-      \032           that had reported problems with display of non-ascii\n\
-      \032           filenames, we'd appreciate knowing if this actually fixes\n\
-      \032           things.)\n\
-      \032         + `-prefer/-force newer' works properly now. (The bug was\n\
-      \032           reported by Sebastian Urbaniak and Sean Fulton.)\n\
-      \032    * User interface and Unison behavior:\n\
-      \032         + Renamed `Proceed' to `Go' in the graphical UI.\n\
-      \032         + Added exit status for the textual user interface.\n\
-      \032         + Paths that are not synchronized because of conflicts or errors\n\
-      \032           during update detection are now noted in the log file.\n\
-      \032         + [END] messages in log now use a briefer format\n\
-      \032         + Changed the text UI startup sequence so that ./unison -ui text\n\
-      \032           will use the default profile instead of failing.\n\
-      \032         + Made some improvements to the error messages.\n\
-      \032         + Added some debugging messages to remote.ml.\n\
-      \n\
-      \032  Changes since 2.7.7:\n\
-      \032    * Incorporated, once again, a multi-threaded transport sub-system. It\n\
-      \032      transfers several files at the same time, thereby making much more\n\
-      \032      effective use of available network bandwidth. Unlike the earlier\n\
-      \032      attempt, this time we do not rely on the native thread library of\n\
-      \032      OCaml. Instead, we implement a light-weight, non-preemptive\n\
-      \032      multi-thread library in OCaml directly. This version appears\n\
-      \032      stable.\n\
-      \032      Some adjustments to unison are made to accommodate the\n\
-      \032      multi-threaded version. These include, in particular, changes to\n\
-      \032      the user interface and logging, for example:\n\
-      \032         + Two log entries for each transferring task, one for the\n\
-      \032           beginning, one for the end.\n\
-      \032         + Suppressed warning messages against removing temp files left\n\
-      \032           by a previous unison run, because warning does not work nicely\n\
-      \032           under multi-threading. The temp file names are made less\n\
-      \032           likely to coincide with the name of a file created by the\n\
-      \032           user. They take the form\n\
-      \032           .#<filename>.<serial>.unison.tmp. [N.b. This was later changed\n\
-      \032           to .unison.<filename>.<serial>.unison.tmp.]\n\
-      \032    * Added a new command to the GTK user interface: pressing 'f' causes\n\
-      \032      Unison to start a new update detection phase, using as paths just\n\
-      \032      those paths that have been detected as changed and not yet marked\n\
-      \032      as successfully completed. Use this command to quickly restart\n\
-      \032      Unison on just the set of paths still needing attention after a\n\
-      \032      previous run.\n\
-      \032    * Made the ignorecase preference user-visible, and changed the\n\
-      \032      initialization code so that it can be manually set to true, even if\n\
-      \032      neither host is running Windows. (This may be useful, e.g., when\n\
-      \032      using Unison running on a Unix system with a FAT volume mounted.)\n\
-      \032    * Small improvements and bug fixes:\n\
-      \032         + Errors in preference files now generate fatal errors rather\n\
-      \032           than warnings at startup time. (I.e., you can't go on from\n\
-      \032           them.) Also, we fixed a bug that was preventing these warnings\n\
-      \032           from appearing in the text UI, so some users who have been\n\
-      \032           running (unsuspectingly) with garbage in their prefs files may\n\
-      \032           now get error reports.\n\
-      \032         + Error reporting for preference files now provides file name\n\
-      \032           and line number.\n\
-      \032         + More intelligible message in the case of identical change to\n\
-      \032           the same files: \"Nothing to do: replicas have been changed\n\
-      \032           only in identical ways since last sync.\"\n\
-      \032         + Files with prefix '.#' excluded when scanning for preference\n\
-      \032           files.\n\
-      \032         + Rsync instructions are send directly instead of first\n\
-      \032           marshaled.\n\
-      \032         + Won't try forever to get the fingerprint of a continuously\n\
-      \032           changing file: unison will give up after certain number of\n\
-      \032           retries.\n\
-      \032         + Other bug fixes, including the one reported by Peter Selinger\n\
-      \032           (force=older preference not working).\n\
-      \032    * Compilation:\n\
-      \032         + Upgraded to the new OCaml 3.04 compiler, with the LablGtk\n\
-      \032           1.2.3 library (patched version used for compiling under\n\
-      \032           Windows).\n\
-      \032         + Added the option to compile unison on the Windows platform\n\
-      \032           with Cygwin GNU C compiler. This option only supports building\n\
-      \032           dynamically linked unison executables.\n\
-      \n\
-      \032  Changes since 2.7.4:\n\
-      \032    * Fixed a silly (but debilitating) bug in the client startup\n\
-      \032      sequence.\n\
-      \n\
-      \032  Changes since 2.7.1:\n\
-      \032    * Added addprefsto preference, which (when set) controls which\n\
-      \032      preference file new preferences (e.g. new ignore patterns) are\n\
-      \032      added to.\n\
-      \032    * Bug fix: read the initial connection header one byte at a time, so\n\
-      \032      that we don't block if the header is shorter than expected. (This\n\
-      \032      bug did not affect normal operation -- it just made it hard to tell\n\
-      \032      when you were trying to use Unison incorrectly with an old version\n\
-      \032      of the server, since it would hang instead of giving an error\n\
-      \032      message.)\n\
-      \n\
-      \032  Changes since 2.6.59:\n\
-      \032    * Changed fastcheck from a boolean to a string preference. Its legal\n\
-      \032      values are yes (for a fast check), no (for a safe check), or\n\
-      \032      default (for a fast check--which also happens to be safe--when\n\
-      \032      running on Unix and a safe check when on Windows). The default is\n\
-      \032      default.\n\
-      \032    * Several preferences have been renamed for consistency. All\n\
-      \032      preference names are now spelled out in lowercase. For backward\n\
-      \032      compatibility, the old names still work, but they are not mentioned\n\
-      \032      in the manual any more.\n\
-      \032    * The temp files created by the 'diff' and 'merge' commands are now\n\
-      \032      named by prepending a new prefix to the file name, rather than\n\
-      \032      appending a suffix. This should avoid confusing diff/merge programs\n\
-      \032      that depend on the suffix to guess the type of the file contents.\n\
-      \032    * We now set the keepalive option on the server socket, to make sure\n\
-      \032      that the server times out if the communication link is unexpectedly\n\
-      \032      broken.\n\
-      \032    * Bug fixes:\n\
-      \032         + When updating small files, Unison now closes the destination\n\
-      \032           file.\n\
-      \032         + File permissions are properly updated when the file is behind\n\
-      \032           a followed link.\n\
-      \032         + Several other small fixes.\n\
-      \n\
-      \032  Changes since 2.6.38:\n\
-      \032    * Major Windows performance improvement!\n\
-      \032      We've added a preference fastcheck that makes Unison look only at a\n\
-      \032      file's creation time and last-modified time to check whether it has\n\
-      \032      changed. This should result in a huge speedup when checking for\n\
-      \032      updates in large replicas.\n\
-      \032      When this switch is set, Unison will use file creation times as\n\
-      \032      'pseudo inode numbers' when scanning Windows replicas for updates,\n\
-      \032      instead of reading the full contents of every file. This may cause\n\
-      \032      Unison to miss propagating an update if the create time,\n\
-      \032      modification time, and length of the file are all unchanged by the\n\
-      \032      update (this is not easy to achieve, but it can be done). However,\n\
-      \032      Unison will never overwrite such an update with a change from the\n\
-      \032      other replica, since it always does a safe check for updates just\n\
-      \032      before propagating a change. Thus, it is reasonable to use this\n\
-      \032      switch most of the time and occasionally run Unison once with\n\
-      \032      fastcheck set to false, if you are worried that Unison may have\n\
-      \032      overlooked an update.\n\
-      \032      Warning: This change is has not yet been thoroughly field-tested.\n\
-      \032      If you set the fastcheck preference, pay careful attention to what\n\
-      \032      Unison is doing.\n\
-      \032    * New functionality: centralized backups and merging\n\
-      \032         + This version incorporates two pieces of major new\n\
-      \032           functionality, implemented by Sylvain Roy during a summer\n\
-      \032           internship at Penn: a centralized backup facility that keeps a\n\
-      \032           full backup of (selected files in) each replica, and a merging\n\
-      \032           feature that allows Unison to invoke an external file-merging\n\
-      \032           tool to resolve conflicting changes to individual files.\n\
-      \032         + Centralized backups:\n\
-      \032              o Unison now maintains full backups of the\n\
-      \032                last-synchronized versions of (some of) the files in each\n\
-      \032                replica; these function both as backups in the usual\n\
-      \032                sense and as the \"common version\" when invoking external\n\
-      \032                merge programs.\n\
-      \032              o The backed up files are stored in a directory\n\
-      \032                /.unison/backup on each host. (The name of this directory\n\
-      \032                can be changed by setting the environment variable\n\
-      \032                UNISONBACKUPDIR.)\n\
-      \032              o The predicate backup controls which files are actually\n\
-      \032                backed up: giving the preference 'backup = Path *' causes\n\
-      \032                backing up of all files.\n\
-      \032              o Files are added to the backup directory whenever unison\n\
-      \032                updates its archive. This means that\n\
-      \032                   # When unison reconstructs its archive from scratch\n\
-      \032                     (e.g., because of an upgrade, or because the archive\n\
-      \032                     files have been manually deleted), all files will be\n\
-      \032                     backed up.\n\
-      \032                   # Otherwise, each file will be backed up the first\n\
-      \032                     time unison propagates an update for it.\n\
-      \032              o The preference backupversions controls how many previous\n\
-      \032                versions of each file are kept. The default is 2 (i.e.,\n\
-      \032                the last synchronized version plus one backup).\n\
-      \032              o For backward compatibility, the backups preference is\n\
-      \032                also still supported, but backup is now preferred.\n\
-      \032              o It is OK to manually delete files from the backup\n\
-      \032                directory (or to throw away the directory itself). Before\n\
-      \032                unison uses any of these files for anything important, it\n\
-      \032                checks that its fingerprint matches the one that it\n\
-      \032                expects.\n\
-      \032         + Merging:\n\
-      \032              o Both user interfaces offer a new 'merge' command, invoked\n\
-      \032                by pressing 'm' (with a changed file selected).\n\
-      \032              o The actual merging is performed by an external program.\n\
-      \032                The preferences merge and merge2 control how this program\n\
-      \032                is invoked. If a backup exists for this file (see the\n\
-      \032                backup preference), then the merge preference is used for\n\
-      \032                this purpose; otherwise merge2 is used. In both cases,\n\
-      \032                the value of the preference should be a string\n\
-      \032                representing the command that should be passed to a shell\n\
-      \032                to invoke the merge program. Within this string, the\n\
-      \032                special substrings CURRENT1, CURRENT2, NEW, and OLD may\n\
-      \032                appear at any point. Unison will substitute these as\n\
-      \032                follows before invoking the command:\n\
-      \032                   # CURRENT1 is replaced by the name of the local copy\n\
-      \032                     of the file;\n\
-      \032                   # CURRENT2 is replaced by the name of a temporary\n\
-      \032                     file, into which the contents of the remote copy of\n\
-      \032                     the file have been transferred by Unison prior to\n\
-      \032                     performing the merge;\n\
-      \032                   # NEW is replaced by the name of a temporary file that\n\
-      \032                     Unison expects to be written by the merge program\n\
-      \032                     when it finishes, giving the desired new contents of\n\
-      \032                     the file; and\n\
-      \032                   # OLD is replaced by the name of the backed up copy of\n\
-      \032                     the original version of the file (i.e., its state at\n\
-      \032                     the end of the last successful run of Unison), if\n\
-      \032                     one exists (applies only to merge, not merge2).\n\
-      \032                For example, on Unix systems setting the merge preference\n\
-      \032                to\n\
-      \032  merge = diff3 -m CURRENT1 OLD CURRENT2 > NEW\n\
-      \n\
-      \032                will tell Unison to use the external diff3 program for\n\
-      \032                merging.\n\
-      \032                A large number of external merging programs are\n\
-      \032                available. For example, emacs users may find the\n\
-      \032                following convenient:\n\
-      \032   merge2 = emacs -q --eval '(ediff-merge-files \"CURRENT1\" \"CURRENT2\"\n\
-      \032              nil \"NEW\")'\n\
-      \032   merge = emacs -q --eval '(ediff-merge-files-with-ancestor\n\
-      \032              \"CURRENT1\" \"CURRENT2\" \"OLD\" nil \"NEW\")'\n\
-      \n\
-      \032                (These commands are displayed here on two lines to avoid\n\
-      \032                running off the edge of the page. In your preference\n\
-      \032                file, each should be written on a single line.)\n\
-      \032              o If the external program exits without leaving any file at\n\
-      \032                the path NEW, Unison considers the merge to have failed.\n\
-      \032                If the merge program writes a file called NEW but exits\n\
-      \032                with a non-zero status code, then Unison considers the\n\
-      \032                merge to have succeeded but to have generated conflicts.\n\
-      \032                In this case, it attempts to invoke an external editor so\n\
-      \032                that the user can resolve the conflicts. The value of the\n\
-      \032                editor preference controls what editor is invoked by\n\
-      \032                Unison. The default is emacs.\n\
-      \032              o Please send us suggestions for other useful values of the\n\
-      \032                merge2 and merge preferences - we'd like to give several\n\
-      \032                examples in the manual.\n\
-      \032    * Smaller changes:\n\
-      \032         + When one preference file includes another, unison no longer\n\
-      \032           adds the suffix '.prf' to the included file by default. If a\n\
-      \032           file with precisely the given name exists in the .unison\n\
-      \032           directory, it will be used; otherwise Unison will add .prf, as\n\
-      \032           it did before. (This change means that included preference\n\
-      \032           files can be named blah.include instead of blah.prf, so that\n\
-      \032           unison will not offer them in its 'choose a preference file'\n\
-      \032           dialog.)\n\
-      \032         + For Linux systems, we now offer both a statically linked and a\n\
-      \032           dynamically linked executable. The static one is larger, but\n\
-      \032           will probably run on more systems, since it doesn't depend on\n\
-      \032           the same versions of dynamically linked library modules being\n\
-      \032           available.\n\
-      \032         + Fixed the force and prefer preferences, which were getting the\n\
-      \032           propagation direction exactly backwards.\n\
-      \032         + Fixed a bug in the startup code that would cause unison to\n\
-      \032           crash when the default profile (~/.unison/default.prf) does\n\
-      \032           not exist.\n\
-      \032         + Fixed a bug where, on the run when a profile is first created,\n\
-      \032           Unison would confusingly display the roots in reverse order in\n\
-      \032           the user interface.\n\
-      \032    * For developers:\n\
-      \032         + We've added a module dependency diagram to the source\n\
-      \032           distribution, in src/DEPENDENCIES.ps, to help new prospective\n\
-      \032           developers with navigating the code.\n\
-      \n\
-      \032  Changes since 2.6.11:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed.\n\
-      \032    * INCOMPATIBLE CHANGE: The startup sequence has been completely\n\
-      \032      rewritten and greatly simplified. The main user-visible change is\n\
-      \032      that the defaultpath preference has been removed. Its effect can be\n\
-      \032      approximated by using multiple profiles, with include directives to\n\
-      \032      incorporate common settings. All uses of defaultpath in existing\n\
-      \032      profiles should be changed to path.\n\
-      \032      Another change in startup behavior that will affect some users is\n\
-      \032      that it is no longer possible to specify roots both in the profile\n\
-      \032      and on the command line.\n\
-      \032      You can achieve a similar effect, though, by breaking your profile\n\
-      \032      into two:\n\
-      \032 default.prf =\n\
-      \032     root = blah\n\
-      \032     root = foo\n\
-      \032     include common\n\
-      \n\
-      \032 common.prf =\n\
-      \032     <everything else>\n\
-      \n\
-      \032      Now do\n\
-      \032 unison common root1 root2\n\
-      \n\
-      \032      when you want to specify roots explicitly.\n\
-      \032    * The -prefer and -force options have been extended to allow users to\n\
-      \032      specify that files with more recent modtimes should be propagated,\n\
-      \032      writing either -prefer newer or -force newer. (For symmetry, Unison\n\
-      \032      will also accept -prefer older or -force older.) The -force\n\
-      \032      older/newer options can only be used when -times is also set.\n\
-      \032      The graphical user interface provides access to these facilities on\n\
-      \032      a one-off basis via the Actions menu.\n\
-      \032    * Names of roots can now be \"aliased\" to allow replicas to be\n\
-      \032      relocated without changing the name of the archive file where\n\
-      \032      Unison stores information between runs. (This feature is for\n\
-      \032      experts only. See the \"Archive Files\" section of the manual for\n\
-      \032      more information.)\n\
-      \032    * Graphical user-interface:\n\
-      \032         + A new command is provided in the Synchronization menu for\n\
-      \032           switching to a new profile without restarting Unison from\n\
-      \032           scratch.\n\
-      \032         + The GUI also supports one-key shortcuts for commonly used\n\
-      \032           profiles. If a profile contains a preference of the form 'key\n\
-      \032           = n', where n is a single digit, then pressing this key will\n\
-      \032           cause Unison to immediately switch to this profile and begin\n\
-      \032           synchronization again from scratch. (Any actions that may have\n\
-      \032           been selected for a set of changes currently being displayed\n\
-      \032           will be discarded.)\n\
-      \032         + Each profile may include a preference 'label = <string>'\n\
-      \032           giving a descriptive string that described the options\n\
-      \032           selected in this profile. The string is listed along with the\n\
-      \032           profile name in the profile selection dialog, and displayed in\n\
-      \032           the top-right corner of the main Unison window.\n\
-      \032    * Minor:\n\
-      \032         + Fixed a bug that would sometimes cause the 'diff' display to\n\
-      \032           order the files backwards relative to the main user interface.\n\
-      \032           (Thanks to Pascal Brisset for this fix.)\n\
-      \032         + On Unix systems, the graphical version of Unison will check\n\
-      \032           the DISPLAY variable and, if it is not set, automatically fall\n\
-      \032           back to the textual user interface.\n\
-      \032         + Synchronization paths (path preferences) are now matched\n\
-      \032           against the ignore preferences. So if a path is both specified\n\
-      \032           in a path preference and ignored, it will be skipped.\n\
-      \032         + Numerous other bugfixes and small improvements.\n\
-      \n\
-      \032  Changes since 2.6.1:\n\
-      \032    * The synchronization of modification times has been disabled for\n\
-      \032      directories.\n\
-      \032    * Preference files may now include lines of the form include <name>,\n\
-      \032      which will cause name.prf to be read at that point.\n\
-      \032    * The synchronization of permission between Windows and Unix now\n\
-      \032      works properly.\n\
-      \032    * A binding CYGWIN=binmode in now added to the environment so that\n\
-      \032      the Cygwin port of OpenSSH works properly in a non-Cygwin context.\n\
-      \032    * The servercmd and addversionno preferences can now be used\n\
-      \032      together: -addversionno appends an appropriate -NNN to the server\n\
-      \032      command, which is found by using the value of the -servercmd\n\
-      \032      preference if there is one, or else just unison.\n\
-      \032    * Both '-pref=val' and '-pref val' are now allowed for boolean\n\
-      \032      values. (The former can be used to set a preference to false.)\n\
-      \032    * Lot of small bugs fixed.\n\
-      \n\
-      \032  Changes since 2.5.31:\n\
-      \032    * The log preference is now set to true by default, since the log\n\
-      \032      file seems useful for most users.\n\
-      \032    * Several miscellaneous bugfixes (most involving symlinks).\n\
-      \n\
-      \032  Changes since 2.5.25:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed (again).\n\
-      \032    * Several significant bugs introduced in 2.5.25 have been fixed.\n\
-      \n\
-      \032  Changes since 2.5.1:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * New functionality:\n\
-      \032         + Unison now synchronizes file modtimes, user-ids, and\n\
-      \032           group-ids.\n\
-      \032           These new features are controlled by a set of new preferences,\n\
-      \032           all of which are currently false by default.\n\
-      \032              o When the times preference is set to true, file\n\
-      \032                modification times are propaged. (Because the\n\
-      \032                representations of time may not have the same granularity\n\
-      \032                on both replicas, Unison may not always be able to make\n\
-      \032                the modtimes precisely equal, but it will get them as\n\
-      \032                close as the operating systems involved allow.)\n\
-      \032              o When the owner preference is set to true, file ownership\n\
-      \032                information is synchronized.\n\
-      \032              o When the group preference is set to true, group\n\
-      \032                information is synchronized.\n\
-      \032              o When the numericIds preference is set to true, owner and\n\
-      \032                group information is synchronized numerically. By\n\
-      \032                default, owner and group numbers are converted to names\n\
-      \032                on each replica and these names are synchronized. (The\n\
-      \032                special user id 0 and the special group 0 are never\n\
-      \032                mapped via user/group names even if this preference is\n\
-      \032                not set.)\n\
-      \032         + Added an integer-valued preference perms that can be used to\n\
-      \032           control the propagation of permission bits. The value of this\n\
-      \032           preference is a mask indicating which permission bits should\n\
-      \032           be synchronized. It is set by default to 0o1777: all bits but\n\
-      \032           the set-uid and set-gid bits are synchronised (synchronizing\n\
-      \032           theses latter bits can be a security hazard). If you want to\n\
-      \032           synchronize all bits, you can set the value of this preference\n\
-      \032           to -1.\n\
-      \032         + Added a log preference (default false), which makes Unison\n\
-      \032           keep a complete record of the changes it makes to the\n\
-      \032           replicas. By default, this record is written to a file called\n\
-      \032           unison.log in the user's home directory (the value of the HOME\n\
-      \032           environment variable). If you want it someplace else, set the\n\
-      \032           logfile preference to the full pathname you want Unison to\n\
-      \032           use.\n\
-      \032         + Added an ignorenot preference that maintains a set of patterns\n\
-      \032           for paths that should definitely not be ignored, whether or\n\
-      \032           not they match an ignore pattern. (That is, a path will now be\n\
-      \032           ignored iff it matches an ignore pattern and does not match\n\
-      \032           any ignorenot patterns.)\n\
-      \032    * User-interface improvements:\n\
-      \032         + Roots are now displayed in the user interface in the same\n\
-      \032           order as they were given on the command line or in the\n\
-      \032           preferences file.\n\
-      \032         + When the batch preference is set, the graphical user interface\n\
-      \032           no longer waits for user confirmation when it displays a\n\
-      \032           warning message: it simply pops up an advisory window with a\n\
-      \032           Dismiss button at the bottom and keeps on going.\n\
-      \032         + Added a new preference for controlling how many status\n\
-      \032           messages are printed during update detection: statusdepth\n\
-      \032           controls the maximum depth for paths on the local machine\n\
-      \032           (longer paths are not displayed, nor are non-directory paths).\n\
-      \032           The value should be an integer; default is 1.\n\
-      \032         + Removed the trace and silent preferences. They did not seem\n\
-      \032           very useful, and there were too many preferences for\n\
-      \032           controlling output in various ways.\n\
-      \032         + The text UI now displays just the default command (the one\n\
-      \032           that will be used if the user just types <return>) instead of\n\
-      \032           all available commands. Typing ? will print the full list of\n\
-      \032           possibilities.\n\
-      \032         + The function that finds the canonical hostname of the local\n\
-      \032           host (which is used, for example, in calculating the name of\n\
-      \032           the archive file used to remember which files have been\n\
-      \032           synchronized) normally uses the gethostname operating system\n\
-      \032           call. However, if the environment variable UNISONLOCALHOSTNAME\n\
-      \032           is set, its value will now be used instead. This makes it\n\
-      \032           easier to use Unison in situations where a machine's name\n\
-      \032           changes frequently (e.g., because it is a laptop and gets\n\
-      \032           moved around a lot).\n\
-      \032         + File owner and group are now displayed in the \"detail window\"\n\
-      \032           at the bottom of the screen, when unison is configured to\n\
-      \032           synchronize them.\n\
-      \032    * For hackers:\n\
-      \032         + Updated to Jacques Garrigue's new version of lablgtk, which\n\
-      \032           means we can throw away our local patched version.\n\
-      \032           If you're compiling the GTK version of unison from sources,\n\
-      \032           you'll need to update your copy of lablgtk to the developers\n\
-      \032           release. (Warning: installing lablgtk under Windows is\n\
-      \032           currently a bit challenging.)\n\
-      \032         + The TODO.txt file (in the source distribution) has been\n\
-      \032           cleaned up and reorganized. The list of pending tasks should\n\
-      \032           be much easier to make sense of, for people that may want to\n\
-      \032           contribute their programming energies. There is also a\n\
-      \032           separate file BUGS.txt for open bugs.\n\
-      \032         + The Tk user interface has been removed (it was not being\n\
-      \032           maintained and no longer compiles).\n\
-      \032         + The debug preference now prints quite a bit of additional\n\
-      \032           information that should be useful for identifying sources of\n\
-      \032           problems.\n\
-      \032         + The version number of the remote server is now checked right\n\
-      \032           away during the connection setup handshake, rather than later.\n\
-      \032           (Somebody sent a bug report of a server crash that turned out\n\
-      \032           to come from using inconsistent versions: better to check this\n\
-      \032           earlier and in a way that can't crash either client or\n\
-      \032           server.)\n\
-      \032         + Unison now runs correctly on 64-bit architectures (e.g. Alpha\n\
-      \032           linux). We will not be distributing binaries for these\n\
-      \032           architectures ourselves (at least for a while) but if someone\n\
-      \032           would like to make them available, we'll be glad to provide a\n\
-      \032           link to them.\n\
-      \032    * Bug fixes:\n\
-      \032         + Pattern matching (e.g. for ignore) is now case-insensitive\n\
-      \032           when Unison is in case-insensitive mode (i.e., when one of the\n\
-      \032           replicas is on a windows machine).\n\
-      \032         + Some people had trouble with mysterious failures during\n\
-      \032           propagation of updates, where files would be falsely reported\n\
-      \032           as having changed during synchronization. This should be\n\
-      \032           fixed.\n\
-      \032         + Numerous smaller fixes.\n\
-      \n\
-      \032  Changes since 2.4.1:\n\
-      \032    * Added a number of 'sorting modes' for the user interface. By\n\
-      \032      default, conflicting changes are displayed at the top, and the rest\n\
-      \032      of the entries are sorted in alphabetical order. This behavior can\n\
-      \032      be changed in the following ways:\n\
-      \032         + Setting the sortnewfirst preference to true causes newly\n\
-      \032           created files to be displayed before changed files.\n\
-      \032         + Setting sortbysize causes files to be displayed in increasing\n\
-      \032           order of size.\n\
-      \032         + Giving the preference sortfirst=<pattern> (where <pattern> is\n\
-      \032           a path descriptor in the same format as 'ignore' and 'follow'\n\
-      \032           patterns, causes paths matching this pattern to be displayed\n\
-      \032           first.\n\
-      \032         + Similarly, giving the preference sortlast=<pattern> causes\n\
-      \032           paths matching this pattern to be displayed last.\n\
-      \032      The sorting preferences are described in more detail in the user\n\
-      \032      manual. The sortnewfirst and sortbysize flags can also be accessed\n\
-      \032      from the 'Sort' menu in the grpahical user interface.\n\
-      \032    * Added two new preferences that can be used to change unison's\n\
-      \032      fundamental behavior to make it more like a mirroring tool instead\n\
-      \032      of a synchronizer.\n\
-      \032         + Giving the preference prefer with argument <root> (by adding\n\
-      \032           -prefer <root> to the command line or prefer=<root>) to your\n\
-      \032           profile) means that, if there is a conflict, the contents of\n\
-      \032           <root> should be propagated to the other replica (with no\n\
-      \032           questions asked). Non-conflicting changes are treated as\n\
-      \032           usual.\n\
-      \032         + Giving the preference force with argument <root> will make\n\
-      \032           unison resolve all differences in favor of the given root,\n\
-      \032           even if it was the other replica that was changed.\n\
-      \032      These options should be used with care! (More information is\n\
-      \032      available in the manual.)\n\
-      \032    * Small changes:\n\
-      \032         + Changed default answer to 'Yes' in all two-button dialogs in\n\
-      \032           the graphical interface (this seems more intuitive).\n\
-      \032         + The rsync preference has been removed (it was used to activate\n\
-      \032           rsync compression for file transfers, but rsync compression is\n\
-      \032           now enabled by default).\n\
-      \032         + In the text user interface, the arrows indicating which\n\
-      \032           direction changes are being propagated are printed differently\n\
-      \032           when the user has overridden Unison's default recommendation\n\
-      \032           (====> instead of ---->). This matches the behavior of the\n\
-      \032           graphical interface, which displays such arrows in a different\n\
-      \032           color.\n\
-      \032         + Carriage returns (Control-M's) are ignored at the ends of\n\
-      \032           lines in profiles, for Windows compatibility.\n\
-      \032         + All preferences are now fully documented in the user manual.\n\
-      \n\
-      \032  Changes since 2.3.12:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * New/improved functionality:\n\
-      \032         + A new preference -sortbysize controls the order in which\n\
-      \032           changes are displayed to the user: when it is set to true, the\n\
-      \032           smallest changed files are displayed first. (The default\n\
-      \032           setting is false.)\n\
-      \032         + A new preference -sortnewfirst causes newly created files to\n\
-      \032           be listed before other updates in the user interface.\n\
-      \032         + We now allow the ssh protocol to specify a port.\n\
-      \032         + Incompatible change: The unison: protocol is deprecated, and\n\
-      \032           we added file: and socket:. You may have to modify your\n\
-      \032           profiles in the .unison directory. If a replica is specified\n\
-      \032           without an explicit protocol, we now assume it refers to a\n\
-      \032           file. (Previously \"//saul/foo\" meant to use SSH to connect to\n\
-      \032           saul, then access the foo directory. Now it means to access\n\
-      \032           saul via a remote file mechanism such as samba; the old effect\n\
-      \032           is now achieved by writing ssh://saul/foo.)\n\
-      \032         + Changed the startup sequence for the case where roots are\n\
-      \032           given but no profile is given on the command line. The new\n\
-      \032           behavior is to use the default profile (creating it if it does\n\
-      \032           not exist), and temporarily override its roots. The manual\n\
-      \032           claimed that this case would work by reading no profile at\n\
-      \032           all, but AFAIK this was never true.\n\
-      \032         + In all user interfaces, files with conflicts are always listed\n\
-      \032           first\n\
-      \032         + A new preference 'sshversion' can be used to control which\n\
-      \032           version of ssh should be used to connect to the server. Legal\n\
-      \032           values are 1 and 2. (Default is empty, which will make unison\n\
-      \032           use whatever version of ssh is installed as the default 'ssh'\n\
-      \032           command.)\n\
-      \032         + The situation when the permissions of a file was updated the\n\
-      \032           same on both side is now handled correctly (we used to report\n\
-      \032           a spurious conflict)\n\
-      \032    * Improvements for the Windows version:\n\
-      \032         + The fact that filenames are treated case-insensitively under\n\
-      \032           Windows should now be handled correctly. The exact behavior is\n\
-      \032           described in the cross-platform section of the manual.\n\
-      \032         + It should be possible to synchronize with Windows shares,\n\
-      \032           e.g., //host/drive/path.\n\
-      \032         + Workarounds to the bug in syncing root directories in Windows.\n\
-      \032           The most difficult thing to fix is an ocaml bug: Unix.opendir\n\
-      \032           fails on c: in some versions of Windows.\n\
-      \032    * Improvements to the GTK user interface (the Tk interface is no\n\
-      \032      longer being maintained):\n\
-      \032         + The UI now displays actions differently (in blue) when they\n\
-      \032           have been explicitly changed by the user from Unison's default\n\
-      \032           recommendation.\n\
-      \032         + More colorful appearance.\n\
-      \032         + The initial profile selection window works better.\n\
-      \032         + If any transfers failed, a message to this effect is displayed\n\
-      \032           along with 'Synchronization complete' at the end of the\n\
-      \032           transfer phase (in case they may have scrolled off the top).\n\
-      \032         + Added a global progress meter, displaying the percentage of\n\
-      \032           total bytes that have been transferred so far.\n\
-      \032    * Improvements to the text user interface:\n\
-      \032         + The file details will be displayed automatically when a\n\
-      \032           conflict is been detected.\n\
-      \032         + when a warning is generated (e.g. for a temporary file left\n\
-      \032           over from a previous run of unison) Unison will no longer wait\n\
-      \032           for a response if it is running in -batch mode.\n\
-      \032         + The UI now displays a short list of possible inputs each time\n\
-      \032           it waits for user interaction.\n\
-      \032         + The UI now quits immediately (rather than looping back and\n\
-      \032           starting the interaction again) if the user presses 'q' when\n\
-      \032           asked whether to propagate changes.\n\
-      \032         + Pressing 'g' in the text user interface will proceed\n\
-      \032           immediately with propagating updates, without asking any more\n\
-      \032           questions.\n\
-      \032    * Documentation and installation changes:\n\
-      \032         + The manual now includes a FAQ, plus sections on common\n\
-      \032           problems and on tricks contributed by users.\n\
-      \032         + Both the download page and the download directory explicitly\n\
-      \032           say what are the current stable and beta-test version numbers.\n\
-      \032         + The OCaml sources for the up-to-the-minute developers' version\n\
-      \032           (not guaranteed to be stable, or even to compile, at any given\n\
-      \032           time!) are now available from the download page.\n\
-      \032         + Added a subsection to the manual describing cross-platform\n\
-      \032           issues (case conflicts, illegal filenames)\n\
-      \032    * Many small bug fixes and random improvements.\n\
-      \n\
-      \032  Changes since 2.3.1:\n\
-      \032    * Several bug fixes. The most important is a bug in the rsync module\n\
-      \032      that would occasionally cause change propagation to fail with a\n\
-      \032      'rename' error.\n\
-      \n\
-      \032  Changes since 2.2:\n\
-      \032    * The multi-threaded transport system is now disabled by default. (It\n\
-      \032      is not stable enough yet.)\n\
-      \032    * Various bug fixes.\n\
-      \032    * A new experimental feature:\n\
-      \032      The final component of a -path argument may now be the wildcard\n\
-      \032      specifier *. When Unison sees such a path, it expands this path on\n\
-      \032      the client into into the corresponding list of paths by listing the\n\
-      \032      contents of that directory.\n\
-      \032      Note that if you use wildcard paths from the command line, you will\n\
-      \032      probably need to use quotes or a backslash to prevent the * from\n\
-      \032      being interpreted by your shell.\n\
-      \032      If both roots are local, the contents of the first one will be used\n\
-      \032      for expanding wildcard paths. (Nb: this is the first one after the\n\
-      \032      canonization step - i.e., the one that is listed first in the user\n\
-      \032      interface - not the one listed first on the command line or in the\n\
-      \032      preferences file.)\n\
-      \n\
-      \032  Changes since 2.1:\n\
-      \032    * The transport subsystem now includes an implementation by Sylvain\n\
-      \032      Gommier and Norman Ramsey of Tridgell and Mackerras's rsync\n\
-      \032      protocol. This protocol achieves much faster transfers when only a\n\
-      \032      small part of a large file has been changed by sending just diffs.\n\
-      \032      This feature is mainly helpful for transfers over slow links--on\n\
-      \032      fast local area networks it can actually degrade performance--so we\n\
-      \032      have left it off by default. Start unison with the -rsync option\n\
-      \032      (or put rsync=true in your preferences file) to turn it on.\n\
-      \032    * \"Progress bars\" are now displayed during remote file transfers,\n\
-      \032      showing what percentage of each file has been transferred so far.\n\
-      \032    * The version numbering scheme has changed. New releases will now be\n\
-      \032      have numbers like 2.2.30, where the second component is incremented\n\
-      \032      on every significant public release and the third component is the\n\
-      \032      \"patch level.\"\n\
-      \032    * Miscellaneous improvements to the GTK-based user interface.\n\
-      \032    * The manual is now available in PDF format.\n\
-      \032    * We are experimenting with using a multi-threaded transport\n\
-      \032      subsystem to transfer several files at the same time, making much\n\
-      \032      more effective use of available network bandwidth. This feature is\n\
-      \032      not completely stable yet, so by default it is disabled in the\n\
-      \032      release version of Unison.\n\
-      \032      If you want to play with the multi-threaded version, you'll need to\n\
-      \032      recompile Unison from sources (as described in the documentation),\n\
-      \032      setting the THREADS flag in Makefile.OCaml to true. Make sure that\n\
-      \032      your OCaml compiler has been installed with the -with-pthreads\n\
-      \032      configuration option. (You can verify this by checking whether the\n\
-      \032      file threads/threads.cma in the OCaml standard library directory\n\
-      \032      contains the string -lpthread near the end.)\n\
-      \n\
-      \032  Changes since 1.292:\n\
-      \032    * Reduced memory footprint (this is especially important during the\n\
-      \032      first run of unison, where it has to gather information about all\n\
-      \032      the files in both repositories).\n\
-      \032    * Fixed a bug that would cause the socket server under NT to fail\n\
-      \032      after the client exits.\n\
-      \032    * Added a SHIFT modifier to the Ignore menu shortcut keys in GTK\n\
-      \032      interface (to avoid hitting them accidentally).\n\
-      \n\
-      \032  Changes since 1.231:\n\
-      \032    * Tunneling over ssh is now supported in the Windows version. See the\n\
-      \032      installation section of the manual for detailed instructions.\n\
-      \032    * The transport subsystem now includes an implementation of the rsync\n\
-      \032      protocol, built by Sylvain Gommier and Norman Ramsey. This protocol\n\
-      \032      achieves much faster transfers when only a small part of a large\n\
-      \032      file has been changed by sending just diffs. The rsync feature is\n\
-      \032      off by default in the current version. Use the -rsync switch to\n\
-      \032      turn it on. (Nb. We still have a lot of tuning to do: you may not\n\
-      \032      notice much speedup yet.)\n\
-      \032    * We're experimenting with a multi-threaded transport subsystem,\n\
-      \032      written by Jerome Vouillon. The downloadable binaries are still\n\
-      \032      single-threaded: if you want to try the multi-threaded version,\n\
-      \032      you'll need to recompile from sources. (Say make THREADS=true.)\n\
-      \032      Native thread support from the compiler is required. Use the option\n\
-      \032      -threads N to select the maximal number of concurrent threads\n\
-      \032      (default is 5). Multi-threaded and single-threaded clients/servers\n\
-      \032      can interoperate.\n\
-      \032    * A new GTK-based user interface is now available, thanks to Jacques\n\
-      \032      Garrigue. The Tk user interface still works, but we'll be shifting\n\
-      \032      development effort to the GTK interface from now on.\n\
-      \032    * OCaml 3.00 is now required for compiling Unison from sources. The\n\
-      \032      modules uitk and myfileselect have been changed to use labltk\n\
-      \032      instead of camltk. To compile the Tk interface in Windows, you must\n\
-      \032      have ocaml-3.00 and tk8.3. When installing tk8.3, put it in c:\\Tcl\n\
-      \032      rather than the suggested c:\\Program Files\\Tcl, and be sure to\n\
-      \032      install the headers and libraries (which are not installed by\n\
-      \032      default).\n\
-      \032    * Added a new -addversionno switch, which causes unison to use\n\
-      \032      unison-<currentversionnumber> instead of just unison as the remote\n\
-      \032      server command. This allows multiple versions of unison to coexist\n\
-      \032      conveniently on the same server: whichever version is run on the\n\
-      \032      client, the same version will be selected on the server.\n\
-      \n\
-      \032  Changes since 1.219:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * This version fixes several annoying bugs, including:\n\
-      \032         + Some cases where propagation of file permissions was not\n\
-      \032           working.\n\
-      \032         + umask is now ignored when creating directories\n\
-      \032         + directories are create writable, so that a read-only directory\n\
-      \032           and its contents can be propagated.\n\
-      \032         + Handling of warnings generated by the server.\n\
-      \032         + Synchronizing a path whose parent is not a directory on both\n\
-      \032           sides is now flagged as erroneous.\n\
-      \032         + Fixed some bugs related to symnbolic links and nonexistent\n\
-      \032           roots.\n\
-      \032              o When a change (deletion or new contents) is propagated\n\
-      \032                onto a 'follow'ed symlink, the file pointed to by the\n\
-      \032                link is now changed. (We used to change the link itself,\n\
-      \032                which doesn't fit our assertion that 'follow' means the\n\
-      \032                link is completely invisible)\n\
-      \032              o When one root did not exist, propagating the other root\n\
-      \032                on top of it used to fail, because unison could not\n\
-      \032                calculate the working directory into which to write\n\
-      \032                changes. This should be fixed.\n\
-      \032    * A human-readable timestamp has been added to Unison's archive\n\
-      \032      files.\n\
-      \032    * The semantics of Path and Name regular expressions now correspond\n\
-      \032      better.\n\
-      \032    * Some minor improvements to the text UI (e.g. a command for going\n\
-      \032      back to previous items)\n\
-      \032    * The organization of the export directory has changed -- should be\n\
-      \032      easier to find / download things now.\n\
-      \n\
-      \032  Changes since 1.200:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * This version has not been tested extensively on Windows.\n\
-      \032    * Major internal changes designed to make unison safer to run at the\n\
-      \032      same time as the replicas are being changed by the user.\n\
-      \032    * Internal performance improvements.\n\
-      \n\
-      \032  Changes since 1.190:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * A number of internal functions have been changed to reduce the\n\
-      \032      amount of memory allocation, especially during the first\n\
-      \032      synchronization. This should help power users with very big\n\
-      \032      replicas.\n\
-      \032    * Reimplementation of low-level remote procedure call stuff, in\n\
-      \032      preparation for adding rsync-like smart file transfer in a later\n\
-      \032      release.\n\
-      \032    * Miscellaneous bug fixes.\n\
-      \n\
-      \032  Changes since 1.180:\n\
-      \032    * INCOMPATIBLE CHANGE: Archive format has changed. Make sure you\n\
-      \032      synchronize your replicas before upgrading, to avoid spurious\n\
-      \032      conflicts. The first sync after upgrading will be slow.\n\
-      \032    * Fixed some small bugs in the interpretation of ignore patterns.\n\
-      \032    * Fixed some problems that were preventing the Windows version from\n\
-      \032      working correctly when click-started.\n\
-      \032    * Fixes to treatment of file permissions under Windows, which were\n\
-      \032      causing spurious reports of different permissions when\n\
-      \032      synchronizing between windows and unix systems.\n\
-      \032    * Fixed one more non-tail-recursive list processing function, which\n\
-      \032      was causing stack overflows when synchronizing very large replicas.\n\
-      \n\
-      \032  Changes since 1.169:\n\
-      \032    * The text user interface now provides commands for ignoring files.\n\
-      \032    * We found and fixed some more non-tail-recursive list processing\n\
-      \032      functions. Some power users have reported success with very large\n\
-      \032      replicas.\n\
-      \032    * INCOMPATIBLE CHANGE: Files ending in .tmp are no longer ignored\n\
-      \032      automatically. If you want to ignore such files, put an appropriate\n\
-      \032      ignore pattern in your profile.\n\
-      \032    * INCOMPATIBLE CHANGE: The syntax of ignore and follow patterns has\n\
-      \032      changed. Instead of putting a line of the form\n\
-      \032                ignore = <regexp>\n\
-      \n\
-      \032      in your profile (.unison/default.prf), you should put:\n\
-      \032                ignore = Regex <regexp>\n\
-      \n\
-      \032      Moreover, two other styles of pattern are also recognized:\n\
-      \032                ignore = Name <name>\n\
-      \n\
-      \032      matches any path in which one component matches <name>, while\n\
-      \032                ignore = Path <path>\n\
-      \n\
-      \032      matches exactly the path <path>.\n\
-      \032      Standard \"globbing\" conventions can be used in <name> and <path>:\n\
-      \032         + a ? matches any single character except /\n\
-      \032         + a * matches any sequence of characters not including /\n\
-      \032         + [xyz] matches any character from the set {x, y, z }\n\
-      \032         + {a,bb,ccc} matches any one of a, bb, or ccc.\n\
-      \032      See the user manual for some examples.\n\
-      \n\
-      \032  Changes since 1.146:\n\
-      \032    * Some users were reporting stack overflows when synchronizing huge\n\
-      \032      directories. We found and fixed some non-tail-recursive list\n\
-      \032      processing functions, which we hope will solve the problem. Please\n\
-      \032      give it a try and let us know.\n\
-      \032    * Major additions to the documentation.\n\
-      \n\
-      \032  Changes since 1.142:\n\
-      \032    * Major internal tidying and many small bugfixes.\n\
-      \032    * Major additions to the user manual.\n\
-      \032    * Unison can now be started with no arguments - it will prompt\n\
-      \032      automatically for the name of a profile file containing the roots\n\
-      \032      to be synchronized. This makes it possible to start the graphical\n\
-      \032      UI from a desktop icon.\n\
-      \032    * Fixed a small bug where the text UI on NT was raising a 'no such\n\
-      \032      signal' exception.\n\
-      \n\
-      \032  Changes since 1.139:\n\
-      \032    * The precompiled windows binary in the last release was compiled\n\
-      \032      with an old OCaml compiler, causing propagation of permissions not\n\
-      \032      to work (and perhaps leading to some other strange behaviors we've\n\
-      \032      heard reports about). This has been corrected. If you're using\n\
-      \032      precompiled binaries on Windows, please upgrade.\n\
-      \032    * Added a -debug command line flag, which controls debugging of\n\
-      \032      various modules. Say -debug XXX to enable debug tracing for module\n\
-      \032      XXX, or -debug all to turn on absolutely everything.\n\
-      \032    * Fixed a small bug where the text UI on NT was raising a 'no such\n\
-      \032      signal' exception.\n\
-      \n\
-      \032  Changes since 1.111:\n\
-      \032    * INCOMPATIBLE CHANGE: The names and formats of the preference files\n\
-      \032      in the .unison directory have changed. In particular:\n\
-      \032         + the file \"prefs\" should be renamed to default.prf\n\
-      \032         + the contents of the file \"ignore\" should be merged into\n\
-      \032           default.prf. Each line of the form REGEXP in ignore should\n\
-      \032           become a line of the form ignore = REGEXP in default.prf.\n\
-      \032    * Unison now handles permission bits and symbolic links. See the\n\
-      \032      manual for details.\n\
-      \032    * You can now have different preference files in your .unison\n\
-      \032      directory. If you start unison like this\n\
-      \032            unison profilename\n\
-      \n\
-      \032      (i.e. with just one \"anonymous\" command-line argument), then the\n\
-      \032      file ~/.unison/profilename.prf will be loaded instead of\n\
-      \032      default.prf.\n\
-      \032    * Some improvements to terminal handling in the text user interface\n\
-      \032    * Added a switch -killServer that terminates the remote server\n\
-      \032      process when the unison client is shutting down, even when using\n\
-      \032      sockets for communication. (By default, a remote server created\n\
-      \032      using ssh/rsh is terminated automatically, while a socket server is\n\
-      \032      left running.)\n\
-      \032    * When started in 'socket server' mode, unison prints 'server\n\
-      \032      started' on stderr when it is ready to accept connections. (This\n\
-      \032      may be useful for scripts that want to tell when a socket-mode\n\
-      \032      server has finished initialization.)\n\
-      \032    * We now make a nightly mirror of our current internal development\n\
-      \032      tree, in case anyone wants an up-to-the-minute version to hack\n\
-      \032      around with.\n\
-      \032    * Added a file CONTRIB with some suggestions for how to help us make\n\
-      \032      Unison better.\n\
+      \032  section \226\128\156Profiles\226\128\157 ). Use your favorite editor for this.\n\
       \n\
       "))
 ::
@@ -4711,12 +3195,11 @@ let docs =
      "Junk\n\
       \032    __________________________________________________________________\n\
       \n\
-      \032    This document was translated from L^AT[E]X by [2]H^EV^EA.\n\
+      \032    This document was translated from L^AT[E]X by [1]H^EV^EA.\n\
       \n\
       References\n\
       \n\
-      \032  1. file:///home/n0/gdt/SOFTWARE/FILESYSTEMS/unison/doc/temp.html#ssh-win\n\
-      \032  2. http://hevea.inria.fr/index.html\n\
+      \032  1. http://hevea.inria.fr/index.html\n\
       "))
 ::
     [];;
diff --git a/src/system.ml b/src/system.ml
index 164a169..e376a7b 100644
--- a/src/system.ml
+++ b/src/system.ml
@@ -15,4 +15,4 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
-include System_impl.System
+include System_impl
diff --git a/src/system/generic/system_impl.ml b/src/system/generic/system_impl.ml
index 09e17b1..31dffe7 100644
--- a/src/system/generic/system_impl.ml
+++ b/src/system/generic/system_impl.ml
@@ -15,9 +15,4 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
-module System = System_generic
-module Fs = struct
-  include System_generic
-
-  let setUnicodeEncoding _ = ()
-end
+include System_generic
diff --git a/src/system/system_generic.ml b/src/system/system_generic.ml
index ae0138c..6f6c689 100644
--- a/src/system/system_generic.ml
+++ b/src/system/system_generic.ml
@@ -17,14 +17,11 @@
 
 type fspath = string
 
-let fspathFromString f = f
-let fspathToPrintString f = f
-let fspathToString f = f
-let fspathToDebugString f = String.escaped f
+let mfspath = Umarshal.string
+
+let extendedPath f = f
 
-let fspathConcat = Filename.concat
-let fspathDirname = Filename.dirname
-let fspathAddSuffixToFinalName f suffix = f ^ suffix
+let fspathToDebugString f = String.escaped f
 
 (****)
 
@@ -71,8 +68,13 @@ let open_in_bin = open_in_bin
 
 let create_process = Unix.create_process
 let open_process_in = Unix.open_process_in
+let open_process_args_in = Unix.open_process_args_in
 let open_process_out = Unix.open_process_out
 let open_process_full cmd = Unix.open_process_full cmd (Unix.environment ())
+let open_process_args_full cmd args = Unix.open_process_args_full cmd args (Unix.environment ())
+let process_in_pid = Unix.process_in_pid
+let process_out_pid = Unix.process_out_pid
+let process_full_pid = Unix.process_full_pid
 let close_process_in = Unix.close_process_in
 let close_process_out = Unix.close_process_out
 let close_process_full = Unix.close_process_full
@@ -81,17 +83,13 @@ let close_process_full = Unix.close_process_full
 
 let isNotWindows = Sys.os_type <> "Win32"
 
-let canSetTime f =
-  isNotWindows ||
-  try
-    Unix.access f [Unix.W_OK];
-    true
-  with
-    Unix.Unix_error _ -> false
-
 (* Note that Cygwin provides some kind of inode numbers, but we only
    have access to the lower 32 bits on 32bit systems... *)
-let hasInodeNumbers () = isNotWindows
+(* Best effort inode numbers are provided in Windows since OCaml 4.03 *)
+(* However, these inode numbers are not usable on FAT filesystems, as
+   renaming a file "b" over a file "a" does not change the inode
+   number of "a". *)
+let hasInodeNumbers () = true
 
 let hasSymlink = Unix.has_symlink
 
@@ -122,10 +120,49 @@ let terminalStateFunctions () =
     startReading = (fun () -> ());
     stopReading = (fun () -> ()) }
 
+let termVtCapable fd = Unix.isatty fd
+
+let has_stdout ~info:_ = true
+let has_stderr ~info:_ = true
+
 (****)
 
 let fingerprint f =
   let ic = open_in_bin f in
-  let d = Digest.channel ic (-1) in
-  close_in ic;
-  d
+  try
+    let d = Digest.channel ic (-1) in
+    close_in ic;
+    d
+  with e ->
+    close_in_noerr ic;
+    raise e
+
+(****)
+
+exception XattrNotSupported
+let _ = Callback.register_exception "XattrNotSupported" XattrNotSupported
+
+external xattr_list : string -> (string * int) list = "unison_xattrs_list"
+external xattr_get_ : string -> string -> string = "unison_xattr_get"
+external xattr_set_ : string -> string -> string -> unit = "unison_xattr_set"
+external xattr_remove_ : string -> string -> unit = "unison_xattr_remove"
+external xattr_updates_ctime : unit -> bool = "unison_xattr_updates_ctime"
+
+let xattrUpdatesCTime = xattr_updates_ctime ()
+
+let xattr_get p n =
+  try xattr_get_ p n with
+  | Failure e -> failwith ("(attr: " ^ n ^ ") " ^ e)
+
+let xattr_set p n v =
+  try xattr_set_ p n v with
+  | Failure e -> failwith ("(attr: " ^ n ^ ") " ^ e)
+
+let xattr_remove p n =
+  try xattr_remove_ p n with
+  | Failure e -> failwith ("(attr: " ^ n ^ ") " ^ e)
+
+(****)
+
+external acl_get_text : string -> string = "unison_acl_to_text"
+external acl_set_text : string -> string -> unit = "unison_acl_from_text"
diff --git a/src/system/system_intf.ml b/src/system/system_intf.ml
index c6c8763..873f4ca 100644
--- a/src/system/system_intf.ml
+++ b/src/system/system_intf.ml
@@ -18,6 +18,7 @@
 module type Core = sig
 
 type fspath
+val mfspath : fspath Umarshal.t
 type dir_handle = { readdir : unit -> string; closedir : unit -> unit }
 
 val symlink : string -> fspath -> unit
@@ -44,7 +45,6 @@ val fingerprint : fspath -> Digest.t
 
 (****)
 
-val canSetTime : fspath -> bool
 val hasInodeNumbers : unit -> bool
 val hasSymlink : unit -> bool
 
@@ -55,23 +55,39 @@ val hasSymlink : unit -> bool
  * [hasCorrectCTime] can have a different value on different systems. *)
 val hasCorrectCTime : bool
 
+(****)
+
+exception XattrNotSupported
+
+val xattr_list : fspath -> (string * int) list
+val xattr_get : fspath -> string -> string
+val xattr_set : fspath -> string -> string -> unit
+val xattr_remove : fspath -> string -> unit
+
+(* [xattrUpdatesCTime] is true if changes to extended attributes update the
+ * file ctime. This means that extended attribute changes can be quickly
+ * detected by looking at ctime change. If file ctime is not updated then
+ * xattrs have to be scanned every time to detect changes. *)
+val xattrUpdatesCTime : bool
+
+(****)
+
+val acl_get_text : fspath -> string
+val acl_set_text : fspath -> string -> unit
+
 end
 
 module type Full = sig
 
-include Core
+include Core with type fspath = string
+
+val extendedPath : string -> fspath
 
 val putenv : string -> string -> unit
 val getenv : string -> string
 val argv : unit -> string array
 
-val fspathFromString : string -> fspath
-val fspathToPrintString : fspath -> string
 val fspathToDebugString : fspath -> string
-val fspathToString : fspath -> string
-val fspathConcat : fspath -> string -> fspath
-val fspathDirname : fspath -> fspath
-val fspathAddSuffixToFinalName : fspath -> string -> fspath
 
 val open_in_gen : open_flag list -> int -> fspath -> in_channel
 
@@ -83,9 +99,15 @@ val create_process :
   string -> string array ->
   Unix.file_descr -> Unix.file_descr -> Unix.file_descr -> int
 val open_process_in : string -> in_channel
+val open_process_args_in : string -> string array -> in_channel
 val open_process_out : string -> out_channel
 val open_process_full :
   string -> in_channel * out_channel * in_channel
+val open_process_args_full :
+  string -> string array -> in_channel * out_channel * in_channel
+val process_in_pid : in_channel -> int
+val process_out_pid : out_channel -> int
+val process_full_pid : in_channel * out_channel * in_channel -> int
 val close_process_in : in_channel -> Unix.process_status
 val close_process_out : out_channel -> Unix.process_status
 val close_process_full :
@@ -96,4 +118,9 @@ type terminalStateFunctions =
     startReading : unit -> unit; stopReading : unit -> unit }
 val terminalStateFunctions : unit -> terminalStateFunctions
 
+val termVtCapable : Unix.file_descr -> bool
+
+val has_stdout : info:string -> bool
+val has_stderr : info:string -> bool
+
 end
diff --git a/src/system/system_win.ml b/src/system/system_win.ml
index 03063ce..72b0061 100644
--- a/src/system/system_win.ml
+++ b/src/system/system_win.ml
@@ -22,61 +22,25 @@ http://www.codeproject.com/KB/cpp/unicode_console_output.aspx?display=Print
 
 *)
 
-module M (P : sig val useLongUNCPaths : bool end) = struct
-
-type fspath = string
-
-let fspathFromString f = f
-let fspathToPrintString f = f
-let fspathToString f = f
-let fspathToDebugString f = String.escaped f
-
-let fspathConcat = Filename.concat
-let fspathDirname = Filename.dirname
-let fspathAddSuffixToFinalName f suffix = f ^ suffix
+include System_generic
 
 (****)
 
-let fixPath f =
-  let length = String.length f in
-  let buffer = Bytes.create length in
-  String.blit f 0 buffer 0 length;
-  for i = 0 to length - 1 do
-    if Bytes.get buffer i = '/' then Bytes.set buffer i '\\'
-  done;
-  Bytes.to_string buffer
+let fixPath f = String.map (function '/' -> '\\' | c -> c) f
 let winRootRx = Rx.rx "[a-zA-Z]:[/\\].*"
-let winUncRx = Rx.rx "[/\\][/\\][^/\\]+[/\\][^/\\]+[/\\].*"
+let winUncRx = Rx.rx "[/\\][/\\][^?/\\]+[/\\][^/\\]+[/\\].*"
+let winFileNsPathRx = Rx.rx "//[?]/.+"
 let extendedPath f =
-  if not P.useLongUNCPaths then
-    f
-  else if Rx.match_string winRootRx f then
+  if Rx.match_string winRootRx f then
     fixPath ("\\\\?\\" ^ f)
   else if Rx.match_string winUncRx f then
     fixPath ("\\\\?\\UNC" ^ String.sub f 1 (String.length f - 1))
+  else if Rx.match_string winFileNsPathRx f then
+    fixPath f
   else
     f
 
-let encodingError p =
-  raise
-    (Sys_error
-       (Format.sprintf "The file path '%s' is not encoded in Unicode." p))
-
-let utf8 = Unicode.from_utf_16
-let utf16 s =
-  try
-    Unicode.to_utf_16 s
-  with Unicode.Invalid ->
-    raise (Sys_error
-             (Format.sprintf "The text '%s' is not encoded in Unicode" s))
-let path8 = Unicode.from_utf_16(*_filename*)
-let path16 f =
-  try Unicode.to_utf_16(*_filename*) f with Unicode.Invalid -> encodingError f
-let epath f =
-  try
-    Unicode.to_utf_16(*_filename*) (extendedPath f)
-  with
-    Unicode.Invalid -> encodingError f
+(****)
 
 let sys_error e =
   match e with
@@ -89,232 +53,102 @@ let sys_error e =
 
 (****)
 
-external getenv_impl : string -> string = "win_getenv"
-external putenv_impl : string -> string -> string -> unit = "win_putenv"
-external argv_impl : unit -> string array = "win_argv"
+external stat_impl : string -> bool -> Unix.LargeFile.stats = "win_stat"
+let stat f = stat_impl f false
+let lstat f = stat_impl f true
 
-let getenv nm = utf8 (getenv_impl (utf16 nm))
-let putenv nm v = putenv_impl nm (utf16 nm) (utf16 v)
-let argv () = Array.map utf8 (argv_impl ())
-
-(****)
+let rename f1 f2 =
+  (* Comment from original C stub implementation:
+     Windows Unicode API: when a file cannot be renamed due to a sharing
+     violation error or an access denied error, retry for up to 1 second,
+     in case the file is temporarily opened by an indexer or an anti-virus. *)
+  let rec ren_aux delay =
+    try
+      Unix.rename f1 f2
+    with
+    | (Unix.Unix_error ((Unix.EACCES | Unix.EUNKNOWNERR (-32)), _, _)) as e ->
+                                       (* ERROR_SHARING_VIOLATION *)
+        if (delay < 1.) then begin
+          Unix.sleepf delay;
+          ren_aux (delay *. 2.)
+        end else
+          raise e
+    | e -> raise e
+  in
+  ren_aux 0.01
 
-type dir_entry = Dir_empty | Dir_read of string | Dir_toread
-type dir_handle = System_generic.dir_handle
-                = { readdir : unit -> string; closedir : unit -> unit }
-
-external stat_impl : string -> string -> bool -> Unix.LargeFile.stats = "win_stat"
-external rmdir_impl : string -> string -> unit = "win_rmdir"
-external mkdir_impl : string -> string -> unit = "win_mkdir"
-external unlink_impl : string -> string -> unit = "win_unlink"
-external rename_impl : string -> string -> string -> unit = "win_rename"
-external link_impl : string -> string -> string -> unit = "win_link"
-external chmod_impl : string -> string -> int -> unit = "win_chmod"
-external utimes_impl :
-  string -> string -> float -> float -> unit = "win_utimes"
-external open_impl :
-  string -> string -> Unix.open_flag list -> Unix.file_perm -> Unix.file_descr = "win_open"
-external chdir_impl : string -> string -> unit = "win_chdir"
-external getcwd_impl : unit -> string = "win_getcwd"
-external findfirst : string -> string * int = "win_findfirstw"
-external findnext : int -> string = "win_findnextw"
-external findclose : int -> unit = "win_findclosew"
-
-let stat f = stat_impl f (epath f) false
-let lstat f = stat_impl f (epath f) true
-let rmdir f = rmdir_impl f (epath f)
-let mkdir f perms = mkdir_impl f (epath f)
-let unlink f = unlink_impl f (epath f)
-let rename f1 f2 = rename_impl f1 (epath f1) (epath f2)
-let chmod f perm = chmod_impl f (epath f) perm
 let chown _ _ _ = raise (Unix.Unix_error (Unix.ENOSYS, "chown", ""))
-let utimes f t1 t2 = utimes_impl f (epath f) t1 t2
-let link f1 f2 = link_impl f1 (epath f1) (epath f2)
-let openfile f flags perm = open_impl f (epath f) flags perm
-let readlink = Unix.readlink
-let symlink f t = Unix.symlink f t
 
-let chdir f =
-  try
-    chdir_impl f (path16 f) (* Better not to use [epath] here *)
-  with e -> sys_error e
+let openfile f flags perm =
+  let fd = Unix.openfile f flags perm in
+  (* Comment from original C stub implementation:
+     Windows: implement somewhat the O_APPEND flag, so that appending
+     lines to a profile (ignored files, for instance) works instead of
+     overwriting the beginning of the file (the file pointer is moved to
+     the end when the file is opened, rather that each time something is
+     written, which is good enough here) *)
+  if List.mem Unix.O_APPEND flags then
+    ignore (Unix.LargeFile.lseek fd 0L Unix.SEEK_END);
+  fd
+
+let readlink f =
+  (* Windows apparently mangles the link values if the value is an absolute
+     path. With [readlink] you're not getting back the same value you set
+     with [symlink] (except if it was a relative path). It's not clear if
+     this happens always or under certain circumstances only.
+
+     It's unclear how this mangling works, but it appears to convert the link
+     value to an NT namespace path under the \?? directory (with \DosDevices
+     being a symlink to it?). For regular DOS paths with a drive letter, this
+     is usually ok in terms of nearly-preserving the original link value, as it
+     only adds the \??\ prefix. For \\server\share\ network paths, it changes
+     the prefix to \??\UNC\server\share\.
+
+     https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-ms-dos-device-names
+     https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/object-directories
+     https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/object-names
+
+     This conversion happens to all(?) absolute paths to targets, whether they
+     were originally in the common DOS format, UNC, or already in Win32 file
+     namespace format (with \\?\ prefix).
+
+     Since we don't know what was the link value set by [symlink], we do as
+     little modification as possible to the output of [readlink]. This means
+     changing the prefix to \\?\ (because that's at least somewhat known to
+     user-space and something we can deal with) and hoping that the resulting
+     path is correct.  Without this change the path will be rejected by some
+     (all?) filesystem syscalls. *)
+  let l = Unix.readlink f in
+  let len = String.length l in
+  if len > 3 && l.[0] = '\\' && l.[1] = '?' && l.[2] = '?' && l.[3] = '\\' then
+    "\\\\?\\" ^ (String.sub l 4 (len - 4))
+  else l
+
+external long_name : string -> string = "win_long_path_name"
 let getcwd () =
   try
-    path8 (getcwd_impl ())
+    (* Normalize the path *)
+    let s = long_name (Sys.getcwd ()) in
+    (* Convert the drive letter to uppercase *)
+    match s.[0] with
+    | 'a' .. 'z' -> String.capitalize_ascii s
+    | _ -> s
   with e -> sys_error e
 
 let badFileRx = Rx.rx ".*[?*].*"
+let winFileNsPathRx = Rx.rx "[/\\][/\\][?][/\\].+"
 
 let opendir d =
-  if Rx.match_string badFileRx d then
+  (* Windows uses wildcards to retrieve the list of files in a directory.
+     It is not possible to list files in a directory when the path name
+     itself contains the wildcards "*" or "?". *)
+  let d' = if Rx.match_string winFileNsPathRx d then String.sub d 4 (String.length d - 4) else d in
+  if Rx.match_string badFileRx d' then
     raise (Unix.Unix_error (Unix.ENOENT, "opendir", d));
-  let (handle, entry_read) =
-    try
-      let (first_entry, handle) = findfirst (epath (fspathConcat d "*")) in
-      (handle, ref (Dir_read first_entry))
-    with End_of_file ->
-      (0, ref Dir_empty)
-  in
-  { readdir =
-      (fun () ->
-         match !entry_read with
-           Dir_empty     -> raise End_of_file
-         | Dir_read name -> entry_read := Dir_toread; path8 name
-         | Dir_toread    -> path8 (findnext handle));
-    closedir =
-      (fun () ->
-         match !entry_read with
-           Dir_empty -> ()
-         | _         -> findclose handle) }
-
-let rec conv_flags fl =
-  match fl with
-    Open_rdonly :: rem   -> Unix.O_RDONLY :: conv_flags rem
-  | Open_wronly :: rem   -> Unix.O_WRONLY :: conv_flags rem
-  | Open_append :: rem   -> Unix.O_APPEND :: conv_flags rem
-  | Open_creat :: rem    -> Unix.O_CREAT :: conv_flags rem
-  | Open_trunc :: rem    -> Unix.O_TRUNC :: conv_flags rem
-  | Open_excl :: rem     -> Unix.O_EXCL :: conv_flags rem
-  | Open_binary :: rem   -> conv_flags rem
-  | Open_text :: rem     -> conv_flags rem
-  | Open_nonblock :: rem -> Unix.O_NONBLOCK :: conv_flags rem
-  | []                   -> []
-
-let open_in_gen flags perms f =
-  try
-    Unix.in_channel_of_descr (openfile f (conv_flags flags) perms)
-  with e ->
-    sys_error e
-let open_out_gen flags perms f =
-  try
-    Unix.out_channel_of_descr (openfile f (conv_flags flags) perms)
-  with e ->
-    sys_error e
-
-(****)
-
-let file_exists f =
-  try
-    ignore (stat f); true
-  with
-    Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
-      false
-  | e ->
-      sys_error e
-
-let open_in_bin f = open_in_gen [Open_rdonly; Open_binary] 0 f
-
-(****)
-
-external win_create_process :
-  string -> string -> string ->
-  Unix.file_descr -> Unix.file_descr -> Unix.file_descr -> int
-  = "w_create_process" "w_create_process_native"
-
-let make_cmdline args =
-  let maybe_quote f =
-    if String.contains f ' ' || String.contains f '\"'
-    then Filename.quote f
-    else f in
-  String.concat " " (List.map maybe_quote (Array.to_list args))
-
-let create_process prog args fd1 fd2 fd3 =
-  win_create_process
-    prog (utf16 prog) (utf16 (make_cmdline args)) fd1 fd2 fd3
+  System_generic.opendir d
 
 (****)
 
-(* The following is by Xavier Leroy and Pascal Cuoq,
-   projet Cristal, INRIA Rocquencourt.
-   Taken from the Objective Caml win32unix library. *)
-
-type popen_process =
-    Process of in_channel * out_channel
-  | Process_in of in_channel
-  | Process_out of out_channel
-  | Process_full of in_channel * out_channel * in_channel
-
-let popen_processes = (Hashtbl.create 7 : (popen_process, int) Hashtbl.t)
-
-let open_proc cmd proc input output error =
-  let shell =
-    try getenv "COMSPEC"
-    with Not_found -> raise(Unix.Unix_error(Unix.ENOEXEC, "open_proc", cmd)) in
-  let pid =
-    win_create_process
-      shell (utf16 shell) (utf16 (shell ^ " /c " ^ cmd)) input output error in
-  Hashtbl.add popen_processes proc pid
-
-let open_process_in cmd =
-  let (in_read, in_write) = Unix.pipe() in
-  Unix.set_close_on_exec in_read;
-  let inchan = Unix.in_channel_of_descr in_read in
-  open_proc cmd (Process_in inchan) Unix.stdin in_write Unix.stderr;
-  Unix.close in_write;
-  inchan
-
-let open_process_out cmd =
-  let (out_read, out_write) = Unix.pipe() in
-  Unix.set_close_on_exec out_write;
-  let outchan = Unix.out_channel_of_descr out_write in
-  open_proc cmd (Process_out outchan) out_read Unix.stdout Unix.stderr;
-  Unix.close out_read;
-  outchan
-
-let open_process_full cmd =
-  let (in_read, in_write) = Unix.pipe() in
-  let (out_read, out_write) = Unix.pipe() in
-  let (err_read, err_write) = Unix.pipe() in
-  Unix.set_close_on_exec in_read;
-  Unix.set_close_on_exec out_write;
-  Unix.set_close_on_exec err_read;
-  let inchan = Unix.in_channel_of_descr in_read in
-  let outchan = Unix.out_channel_of_descr out_write in
-  let errchan = Unix.in_channel_of_descr err_read in
-  open_proc cmd (Process_full(inchan, outchan, errchan))
-                out_read in_write err_write;
-  Unix.close out_read; Unix.close in_write; Unix.close err_write;
-  (inchan, outchan, errchan)
-
-let find_proc_id fun_name proc =
-  try
-    let pid = Hashtbl.find popen_processes proc in
-    Hashtbl.remove popen_processes proc;
-    pid
-  with Not_found ->
-    raise(Unix.Unix_error(Unix.EBADF, fun_name, ""))
-
-let close_process_in inchan =
-  let pid = find_proc_id "close_process_in" (Process_in inchan) in
-  close_in inchan;
-  snd(Unix.waitpid [] pid)
-
-let close_process_out outchan =
-  let pid = find_proc_id "close_process_out" (Process_out outchan) in
-  close_out outchan;
-  snd(Unix.waitpid [] pid)
-
-let close_process_full (inchan, outchan, errchan) =
-  let pid =
-    find_proc_id "close_process_full"
-                 (Process_full(inchan, outchan, errchan)) in
-  close_in inchan; close_out outchan; close_in errchan;
-  snd(Unix.waitpid [] pid)
-
-(****)
-
-(* The new implementation of utimes does not have the limitation of
-   the standard one *)
-let canSetTime f = true
-
-(* We provide some kind of inode numbers *)
-(* However, these inode numbers are not usable on FAT filesystems, as
-   renaming a file "b" over a file "a" does not change the inode
-   number of "a". *)
-let hasInodeNumbers () = true
-
-let hasSymlink = Unix.has_symlink
-
 external hasCorrectCTime_impl : unit -> bool = "win_has_correct_ctime"
 
 let hasCorrectCTime = hasCorrectCTime_impl ()
@@ -328,9 +162,8 @@ external setConsoleMode : int -> unit = "win_set_console_mode"
 external getConsoleOutputCP : unit -> int = "win_get_console_output_cp"
 external setConsoleOutputCP : int -> unit = "win_set_console_output_cp"
 
-type terminalStateFunctions =
-  { defaultTerminal : unit -> unit; rawTerminal : unit -> unit;
-    startReading : unit -> unit; stopReading : unit -> unit }
+external termVtCapable : Unix.file_descr -> bool = "win_vt_capable"
+(* [termVtCapable] is for _output_ file descriptors. *)
 
 let terminalStateFunctions () =
   (* First, allocate a console in case we don't already have one.
@@ -357,6 +190,8 @@ let terminalStateFunctions () =
   let () = redirect (initConsole ()) in
   let oldstate = getConsoleMode () in
   let oldcp = getConsoleOutputCP () in
+  (* 0x200 = ENABLE_VIRTUAL_TERMINAL_INPUT *)
+  let vtin = if termVtCapable Unix.stdout then 0x200 else 0x0 in
   (* Ctrl-C does not interrupt a call to ReadFile when
      ENABLE_LINE_INPUT is not set, so we handle Ctr-C
      as a character when reading from the console.
@@ -366,15 +201,8 @@ let terminalStateFunctions () =
   { defaultTerminal = (fun () -> setConsoleMode oldstate;
                                  setConsoleOutputCP oldcp);
     rawTerminal = (fun () -> setConsoleMode 0x19; setConsoleOutputCP 65001);
-    startReading = (fun () -> setConsoleMode 0x18);
+    startReading = (fun () -> setConsoleMode (0x18 lor vtin));
     stopReading = (fun () -> setConsoleMode 0x19) }
 
-(****)
-
-let fingerprint f =
-  let ic = open_in_bin f in
-  let d = Digest.channel ic (-1) in
-  close_in ic;
-  d
-
-end
+external has_stdout : info:string -> bool = "win_hasconsole_gui_stdout"
+external has_stderr : info:string -> bool = "win_hasconsole_gui_stderr"
diff --git a/src/system/system_win_stubs.c b/src/system/system_win_stubs.c
index a8c1d31..357d850 100644
--- a/src/system/system_win_stubs.c
+++ b/src/system/system_win_stubs.c
@@ -1,236 +1,28 @@
 #define WINVER 0x0500
 
-#include <caml/mlvalues.h>
-#include <caml/alloc.h>
-#include <caml/memory.h>
-#include <caml/fail.h>
-
+#include <winsock2.h>
 #include <windows.h>
 #include <fcntl.h>
 #include <sys/stat.h>
 #include <stdio.h>
 #include <stdint.h>
 
-#define NT_MAX_PATH 32768
-
-#define Nothing ((value) 0)
-
-struct filedescr {
-  union {
-    HANDLE handle;
-    SOCKET socket;
-  } fd;
-  enum { KIND_HANDLE, KIND_SOCKET } kind;
-  int crt_fd;
-};
-#define Handle_val(v) (((struct filedescr *) Data_custom_val(v))->fd.handle)
-
-value copy_wstring(LPCWSTR s)
-{
-  int len;
-  value res;
-
-  len = 2 * wcslen(s) + 2;  /* NULL character included */
-  res = caml_alloc_string(len);
-  memmove((char *)String_val(res), s, len);
-  return res;
-}
-
-extern void win32_maperr (DWORD errcode);
-extern void uerror (char * cmdname, value arg);
-extern value win_alloc_handle (HANDLE h);
-extern value cst_to_constr (int n, int * tbl, int size, int deflt);
-
-static int open_access_flags[12] = {
-  GENERIC_READ, GENERIC_WRITE, GENERIC_READ|GENERIC_WRITE,
-  0, 0, 0, 0, 0, 0, 0, 0, 0
-};
-
-static int open_create_flags[12] = {
-  0, 0, 0, 0, O_APPEND, O_CREAT, O_TRUNC, O_EXCL, 0, 0, 0, 0
-};
-
-/****/
-
-CAMLprim value win_rmdir(value path, value wpath)
-{
-  CAMLparam2(path, wpath);
-  if (!RemoveDirectoryW((LPWSTR)String_val(wpath))) {
-    win32_maperr (GetLastError ());
-    uerror("rmdir", path);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_mkdir(value path, value wpath)
-{
-  CAMLparam2(path, wpath);
-  if (!CreateDirectoryW((LPWSTR)String_val(wpath), NULL)) {
-    win32_maperr (GetLastError ());
-    uerror("mkdir", path);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_unlink(value path, value wpath)
-{
-  CAMLparam2(path, wpath);
-  if (!DeleteFileW((LPWSTR)String_val(wpath))) {
-    win32_maperr (GetLastError ());
-    uerror("unlink", path);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_rename(value path1, value wpath1, value wpath2)
-{
-  int err, t;
-  CAMLparam3(path1, wpath1, wpath2);
-
-  t = 10;
- retry:
-  if (!MoveFileExW((LPWSTR)String_val(wpath1), (LPWSTR)String_val(wpath2),
-                  MOVEFILE_REPLACE_EXISTING)) {
-    err = GetLastError ();
-    if ((err == ERROR_SHARING_VIOLATION || err == ERROR_ACCESS_DENIED) &&
-        t < 1000) {
-      /* The renaming may fail due to an indexer or an anti-virus.
-         We retry after a short time in the hope that this other
-         program is done with the file. */
-      Sleep (t);
-      t *= 2;
-      goto retry;
-    }
-    win32_maperr (err);
-    uerror("rename", path1);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_link(value path1, value wpath1, value wpath2)
-{
-  CAMLparam3(path1, wpath1, wpath2);
-
-  if (!CreateHardLinkW((LPWSTR)String_val(wpath2), (LPWSTR)String_val(wpath1),
-                       NULL)) {
-    win32_maperr (GetLastError ());
-    uerror("rename", path1);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_chmod (value path, value wpath, value perm) {
-  DWORD attr;
-  CAMLparam3(path, wpath, perm);
-
-  attr = GetFileAttributesW ((LPCWSTR)String_val (wpath));
-  if (attr == INVALID_FILE_ATTRIBUTES) {
-    win32_maperr (GetLastError ());
-    uerror("chmod", path);
-  }
-  if (Int_val(perm) & _S_IWRITE)
-    attr &= ~FILE_ATTRIBUTE_READONLY;
-  else
-    attr |= FILE_ATTRIBUTE_READONLY;
-
-  if (!SetFileAttributesW ((LPCWSTR)String_val (wpath), attr)) {
-    win32_maperr (GetLastError ());
-    uerror("chmod", path);
-  }
-
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_utimes (value path, value wpath, value atime, value mtime) {
-  HANDLE h;
-  BOOL res;
-  ULARGE_INTEGER iatime, imtime;
-  FILETIME fatime, fmtime;
-
-  CAMLparam4(path, wpath, atime, mtime);
-
-  iatime.QuadPart = Double_val(atime);
-  imtime.QuadPart = Double_val(mtime);
-
-  /* http://www.filewatcher.com/p/Win32-UTCFileTime-1.44.tar.gz.93147/Win32-UTCFileTime-1.44/UTCFileTime.xs.html */
-  /* http://savannah.nongnu.org/bugs/?22781#comment0 */
-  if (iatime.QuadPart || imtime.QuadPart) {
-    iatime.QuadPart += 11644473600ull;
-    iatime.QuadPart *= 10000000ull;
-    fatime.dwLowDateTime = iatime.LowPart;
-    fatime.dwHighDateTime = iatime.HighPart;
-    imtime.QuadPart += 11644473600ull;
-    imtime.QuadPart *= 10000000ull;
-    fmtime.dwLowDateTime = imtime.LowPart;
-    fmtime.dwHighDateTime = imtime.HighPart;
-  } else {
-    GetSystemTimeAsFileTime (&fatime);
-    fmtime = fatime;
-  }
-  h = CreateFileW ((LPWSTR) wpath, FILE_WRITE_ATTRIBUTES,
-                   FILE_SHARE_READ | FILE_SHARE_WRITE,
-                   NULL, OPEN_EXISTING, 0, NULL);
-  if (h == INVALID_HANDLE_VALUE) {
-    win32_maperr (GetLastError ());
-    uerror("utimes", path);
-  }
-  res = SetFileTime (h, NULL, &fatime, &fmtime);
-  if (res == 0) {
-    win32_maperr (GetLastError ());
-    (void)CloseHandle (h);
-    uerror("utimes", path);
-  }
-  res = CloseHandle (h);
-  if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("utimes", path);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_open (value path, value wpath, value flags, value perm) {
-  int fileaccess, createflags, fileattrib, filecreate;
-  SECURITY_ATTRIBUTES attr;
-  HANDLE h;
-
-  CAMLparam4 (path, wpath, flags, perm);
-
-  fileaccess = convert_flag_list(flags, open_access_flags);
-
-  createflags = convert_flag_list(flags, open_create_flags);
-  if ((createflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL))
-    filecreate = CREATE_NEW;
-  else if ((createflags & (O_CREAT | O_TRUNC)) == (O_CREAT | O_TRUNC))
-    filecreate = CREATE_ALWAYS;
-  else if (createflags & O_TRUNC)
-    filecreate = TRUNCATE_EXISTING;
-  else if (createflags & O_CREAT)
-    filecreate = OPEN_ALWAYS;
-  else
-    filecreate = OPEN_EXISTING;
-
-  if ((createflags & O_CREAT) && (Int_val(perm) & 0200) == 0)
-    fileattrib = FILE_ATTRIBUTE_READONLY;
-  else
-    fileattrib = FILE_ATTRIBUTE_NORMAL;
-
-  attr.nLength = sizeof(attr);
-  attr.lpSecurityDescriptor = NULL;
-  attr.bInheritHandle = TRUE;
-
-  h = CreateFileW((LPCWSTR) String_val(wpath), fileaccess,
-                  FILE_SHARE_READ | FILE_SHARE_WRITE, &attr,
-                  filecreate, fileattrib, NULL);
-
-  if (h == INVALID_HANDLE_VALUE) {
-    win32_maperr (GetLastError ());
-    uerror("open", path);
-  }
-
-  if (createflags & O_APPEND) SetFilePointer (h, 0, NULL, FILE_END);
+#include <caml/mlvalues.h>
+#include <caml/alloc.h>
+#include <caml/memory.h>
+#include <caml/fail.h>
+#include <caml/unixsupport.h>
+#include <caml/version.h>
+#if OCAML_VERSION < 41300
+#define CAML_INTERNALS /* was needed from OCaml 4.06 to 4.12 */
+#endif
+#include <caml/osdeps.h>
 
-  CAMLreturn(win_alloc_handle(h));
-}
+#if OCAML_VERSION_MAJOR < 5
+#define caml_uerror uerror
+#define caml_win32_maperr win32_maperr
+#define caml_win32_alloc_handle win_alloc_handle
+#endif
 
 
 /* Parts of code in the following section are originally copied from libuv.
@@ -382,6 +174,8 @@ typedef enum _FILE_INFORMATION_CLASS {
   FileMaximumInformation
 } FILE_INFORMATION_CLASS, *PFILE_INFORMATION_CLASS;
 
+#if !defined(OCAML_VERSION) || OCAML_VERSION < 40300 || OCAML_VERSION >= 41400
+
 typedef struct _REPARSE_DATA_BUFFER {
   ULONG  ReparseTag;
   USHORT ReparseDataLength;
@@ -412,6 +206,8 @@ typedef struct _REPARSE_DATA_BUFFER {
   };
 } REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
 
+#endif /* !OCAML_VERSION */
+
 typedef NTSTATUS (NTAPI *sNtQueryInformationFile)
                  (HANDLE FileHandle,
                   PIO_STATUS_BLOCK IoStatusBlock,
@@ -470,7 +266,7 @@ void win_init()
 
 /* END section originally copied from libuv win/winapi.c */
 
-CAMLprim value win_has_correct_ctime()
+CAMLprim value win_has_correct_ctime(value unit)
 {
   CAMLparam0();
 
@@ -480,18 +276,18 @@ CAMLprim value win_has_correct_ctime()
 }
 
 #define MAKEDWORDLONG(a,b) ((DWORDLONG)(((DWORD)(a))|(((DWORDLONG)((DWORD)(b)))<<32)))
-#define WINTIME_TO_TIME(t) (((ULONGLONG) t) / 10000000ull - 11644473600ull)
+#define WINTIME_TO_TIME(t) ((((ULONGLONG) t) - 116444736000000000ull) / 10000000ull)
 #define FILETIME_TO_TIME(ft) WINTIME_TO_TIME((((ULONGLONG) ft.dwHighDateTime) << 32) + ft.dwLowDateTime)
 #define FILETIME_NT_TO_TIME(ft) WINTIME_TO_TIME(ft.QuadPart)
 
-CAMLprim value win_stat(value path, value wpath, value lstat)
+CAMLprim value win_stat(value path, value lstat)
 {
   uintnat dev;
   uintnat ino;
   uintnat kind;
   uintnat mode;
   uintnat nlink;
-  uint64_t size;
+  uint64_t size = 0;
   double atime;
   double mtime;
   double ctime;
@@ -503,21 +299,24 @@ CAMLprim value win_stat(value path, value wpath, value lstat)
   BY_HANDLE_FILE_INFORMATION info;
   IO_STATUS_BLOCK io_status;
   FILE_ALL_INFORMATION file_info;
-  CAMLparam3(path,wpath, lstat);
+  CAMLparam2(path, lstat);
   CAMLlocal1 (v);
   char *fname = Bool_val(lstat) ? "lstat" : "stat";
 
   win_init();
 
-  h = CreateFileW ((LPCWSTR) String_val (wpath), FILE_READ_ATTRIBUTES,
+  wchar_t *wpath = caml_stat_strdup_to_utf16(String_val(path));
+
+  h = CreateFileW (wpath, FILE_READ_ATTRIBUTES,
                    FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
                    NULL, OPEN_EXISTING,
                    FILE_FLAG_BACKUP_SEMANTICS | FILE_ATTRIBUTE_READONLY |
                    (Bool_val(lstat) ? FILE_FLAG_OPEN_REPARSE_POINT : 0), NULL);
+  caml_stat_free(wpath);
 
   if (h == INVALID_HANDLE_VALUE) {
-    win32_maperr (GetLastError ());
-    uerror(fname, path);
+    caml_win32_maperr(GetLastError());
+    caml_uerror(fname, path);
   }
 
   if (nt_api_available) {
@@ -526,17 +325,17 @@ CAMLprim value win_stat(value path, value wpath, value lstat)
 
     /* Buffer overflow (a warning status code) is expected here. */
     if (NT_ERROR(nt_status)) {
-      win32_maperr(pRtlNtStatusToDosError(nt_status));
+      caml_win32_maperr(pRtlNtStatusToDosError(nt_status));
       (void) CloseHandle(h);
-      uerror(fname, path);
+      caml_uerror(fname, path);
     }
   }
 
   res = GetFileInformationByHandle (h, &info);
   if (res == 0) {
-    win32_maperr (GetLastError ());
+    caml_win32_maperr(GetLastError());
     (void) CloseHandle (h);
-    uerror(fname, path);
+    caml_uerror(fname, path);
   }
 
   if (Bool_val(lstat) &&
@@ -557,12 +356,12 @@ CAMLprim value win_stat(value path, value wpath, value lstat)
 
   res = CloseHandle (h);
   if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror(fname, path);
+    caml_win32_maperr(GetLastError());
+    caml_uerror(fname, path);
   }
 
   if (Bool_val(lstat) && !syml) {
-    CAMLreturn(win_stat(path, wpath, Val_false));
+    CAMLreturn(win_stat(path, Val_false));
   }
 
   dev = info.dwVolumeSerialNumber;
@@ -631,217 +430,43 @@ CAMLprim value win_stat(value path, value wpath, value lstat)
   Store_field(v, 5, Val_int(0));
   Store_field(v, 6, Val_int(0));
   Store_field(v, 7, Val_int(0));
-  Store_field(v, 8, copy_int64(size));
-  Store_field(v, 9, copy_double(atime));
-  Store_field(v, 10, copy_double(mtime));
-  Store_field(v, 11, copy_double(ctime));
+  Store_field(v, 8, caml_copy_int64(size));
+  Store_field(v, 9, caml_copy_double(atime));
+  Store_field(v, 10, caml_copy_double(mtime));
+  Store_field(v, 11, caml_copy_double(ctime));
 
   CAMLreturn (v);
 }
 
-CAMLprim value win_chdir (value path, value wpath)
-{
-  CAMLparam2(path,wpath);
-  if (!SetCurrentDirectoryW ((LPWSTR)wpath)) {
-    win32_maperr(GetLastError());
-    uerror("chdir", path);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_getcwd (value unit)
-{
-  int res;
-  wchar_t s[NT_MAX_PATH];
-  CAMLparam0();
-  CAMLlocal1 (path);
-
-  res = GetCurrentDirectoryW (NT_MAX_PATH, s);
-  if (res == 0) {
-    win32_maperr(GetLastError());
-    uerror("getcwd", Nothing);
-  }
-  /* Normalize the path */
-  res = GetLongPathNameW (s, s, NT_MAX_PATH);
-  if (res == 0) {
-    win32_maperr(GetLastError());
-    uerror("getcwd", Nothing);
-  }
-  /* Convert the drive letter to uppercase */
-  if (s[0] >= L'a' && s[0] <= L'z') s[0] -= 32;
-  path = copy_wstring(s);
-  CAMLreturn (path);
-}
-
-CAMLprim value win_findfirstw(value name)
-{
-  HANDLE h;
-  WIN32_FIND_DATAW fileinfo;
-
-  CAMLparam1(name);
-  CAMLlocal3(v, valname, valh);
-
-  h = FindFirstFileW((LPCWSTR) String_val(name),&fileinfo);
-  if (h == INVALID_HANDLE_VALUE) {
-    DWORD err = GetLastError();
-    if ((err == ERROR_NO_MORE_FILES) || (err == ERROR_FILE_NOT_FOUND))
-      raise_end_of_file();
-    else {
-      win32_maperr(err);
-      uerror("opendir", Nothing);
-    }
-  }
-  valname = copy_wstring(fileinfo.cFileName);
-  valh = win_alloc_handle(h);
-  v = alloc_small(2, 0);
-  Field(v,0) = valname;
-  Field(v,1) = valh;
-  CAMLreturn (v);
-}
-
-CAMLprim value win_findnextw(value valh)
-{
-  WIN32_FIND_DATAW fileinfo;
-  BOOL retcode;
-
-  CAMLparam1(valh);
-
-  retcode = FindNextFileW(Handle_val(valh), &fileinfo);
-  if (!retcode) {
-    DWORD err = GetLastError();
-    if (err == ERROR_NO_MORE_FILES)
-      raise_end_of_file();
-    else {
-      win32_maperr(err);
-      uerror("readdir", Nothing);
-    }
-  }
-  CAMLreturn (copy_wstring(fileinfo.cFileName));
-}
-
-CAMLprim value win_findclosew(value valh)
-{
-  CAMLparam1(valh);
-
-  if (! FindClose(Handle_val(valh))) {
-    win32_maperr(GetLastError());
-    uerror("closedir", Nothing);
-  }
-  CAMLreturn (Val_unit);
-}
-
-CAMLprim value win_getenv(value var)
-{
-  LPWSTR s;
-  DWORD len;
-  CAMLparam1(var);
-  CAMLlocal1(res);
-
-  s = stat_alloc (65536);
-
-  len = GetEnvironmentVariableW((LPCWSTR) String_val(var), s, 65536);
-  if (len == 0) { stat_free (s); raise_not_found(); }
-
-  res = copy_wstring(s);
-  stat_free (s);
-  CAMLreturn (res);
-
-}
-
-CAMLprim value win_putenv(value var, value wvar, value v)
-{
-  BOOL res;
-  CAMLparam3(var, wvar, v);
-
-  res = SetEnvironmentVariableW((LPCWSTR) String_val(wvar), (LPCWSTR) v);
-  if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("putenv", var);
-  }
-  CAMLreturn (Val_unit);
-}
+/****/
 
-CAMLprim value win_argv(value unit)
+static value win_hasconsole_gui_msg(DWORD h, const char *s)
 {
-  int n, i;
-  LPWSTR * l;
-
-  CAMLparam0();
-  CAMLlocal2(v,res);
-
-  l = CommandLineToArgvW (GetCommandLineW (), &n);
-
-  if (l == NULL) {
-    win32_maperr (GetLastError ());
-    uerror("argv", Nothing);
-  }
-  res = caml_alloc (n, 0);
-  for (i = 0; i < n; i++) {
-    v = copy_wstring (l[i]);
-    Store_field (res, i, v);
+  const char *u = "This is a GUI-only executable. Text console output "
+                  "is not supported. To get text output, use the "
+                  "executable intended for it (usually called unison.exe "
+                  "or unison-text.exe) or redirect the output.";
+
+  if (!GetFileType((HANDLE) GetStdHandle(h))) {
+    MessageBoxA(NULL, strcmp(s, "") != 0 ? s : u, "Information", MB_OK);
+    return Val_false;
+  } else {
+    return Val_true;
   }
-  LocalFree (l);
-  CAMLreturn (res);
 }
 
-CAMLprim value w_create_process_native
-(value prog, value wprog, value wargs, value fd1, value fd2, value fd3)
+CAMLprim value win_hasconsole_gui_stdout(value s)
 {
-  int res, flags;
-  PROCESS_INFORMATION pi;
-  STARTUPINFOW si;
-  wchar_t fullname [MAX_PATH];
-  HANDLE h;
-  CAMLparam5(wprog, wargs, fd1, fd2, fd3);
-
-  res = SearchPathW (NULL, (LPCWSTR) String_val(wprog), L".exe",
-                     MAX_PATH, fullname, NULL);
-  if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("create_process", prog);
-  }
-
-  ZeroMemory(&si, sizeof(STARTUPINFO));
-
-  si.cb = sizeof(STARTUPINFO);
-  si.dwFlags = STARTF_USESTDHANDLES;
-  si.hStdInput = Handle_val(fd1);
-  si.hStdOutput = Handle_val(fd2);
-  si.hStdError = Handle_val(fd3);
-
-  flags = GetPriorityClass (GetCurrentProcess ());
-  /*
-  h = CreateFile ("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, NULL,
-                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
-  if (h != INVALID_HANDLE_VALUE)
-    CloseHandle (h);
-  else {
-    flags |= CREATE_NEW_CONSOLE;
-    //    si.dwFlags |= STARTF_USESHOWWINDOW;
-    //    si.wShowWindow = SW_MINIMIZE;
-  }
-  */
-
-  res = CreateProcessW (fullname, (LPWSTR) String_val(wargs),
-                        NULL, NULL, TRUE, flags,
-                        NULL, NULL, &si, &pi);
-  if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("create_process", prog);
-  }
-
-  CloseHandle (pi.hThread);
-  CAMLreturn (Val_long (pi.hProcess));
+  CAMLparam1(s);
+  CAMLreturn(win_hasconsole_gui_msg(STD_OUTPUT_HANDLE, String_val(s)));
 }
 
-CAMLprim value w_create_process(value * argv, int argn)
+CAMLprim value win_hasconsole_gui_stderr(value s)
 {
-  return w_create_process_native(argv[0], argv[1], argv[2],
-                                 argv[3], argv[4], argv[5]);
+  CAMLparam1(s);
+  CAMLreturn(win_hasconsole_gui_msg(STD_ERROR_HANDLE, String_val(s)));
 }
 
-/****/
-
 CAMLprim value win_init_console(value unit)
 {
   CAMLparam0();
@@ -885,18 +510,18 @@ CAMLprim value win_init_console(value unit)
 
     /* Return only handles that are not already redirected by user. */
     if (!GetFileType(in_orig)) {
-      tmp = caml_alloc_small(1, 0);
-      Store_field(tmp, 0, win_alloc_handle(in));
+      tmp = caml_alloc(1, 0);
+      Store_field(tmp, 0, caml_win32_alloc_handle(in));
       Store_field(ret, 0, tmp);
     }
     if (!GetFileType(out_orig)) {
-      tmp = caml_alloc_small(1, 0);
-      Store_field(tmp, 0, win_alloc_handle(out));
+      tmp = caml_alloc(1, 0);
+      Store_field(tmp, 0, caml_win32_alloc_handle(out));
       Store_field(ret, 1, tmp);
     }
     if (!GetFileType(err_orig)) {
-      tmp = caml_alloc_small(1, 0);
-      Store_field(tmp, 0, win_alloc_handle(err));
+      tmp = caml_alloc(1, 0);
+      Store_field(tmp, 0, caml_win32_alloc_handle(err));
       Store_field(ret, 2, tmp);
     }
   }
@@ -913,14 +538,15 @@ static void init_conin ()
                         FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
                         OPEN_EXISTING, 0, 0);
     if (conin == INVALID_HANDLE_VALUE) {
-      win32_maperr (GetLastError ());
-      uerror("init_conin", Nothing);
+      caml_win32_maperr(GetLastError());
+      caml_uerror("init_conin", Nothing);
     }
   }
 }
 
 CAMLprim value win_get_console_mode (value unit)
 {
+  CAMLparam0();
   DWORD mode;
   BOOL res;
 
@@ -928,37 +554,56 @@ CAMLprim value win_get_console_mode (value unit)
 
   res = GetConsoleMode (conin, &mode);
   if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("get_console_mode", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("get_console_mode", Nothing);
   }
 
-  return (Val_int (mode));
+  CAMLreturn(Val_int(mode));
 }
 
 CAMLprim value win_set_console_mode (value mode)
 {
+  CAMLparam1(mode);
   BOOL res;
 
   init_conin ();
 
   res = SetConsoleMode (conin, Int_val(mode));
   if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("set_console_mode", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("set_console_mode", Nothing);
   }
-  return (Val_unit);
+  CAMLreturn(Val_unit);
 }
 
 CAMLprim value win_get_console_output_cp (value unit) {
-  return (Val_int (GetConsoleOutputCP ()));
+  CAMLparam0();
+  CAMLreturn(Val_int(GetConsoleOutputCP()));
 }
 
 CAMLprim value win_set_console_output_cp (value cp) {
+  CAMLparam1(cp);
   BOOL res;
   res = SetConsoleOutputCP (Int_val (cp));
   if (res == 0) {
-    win32_maperr (GetLastError ());
-    uerror("set_console_cp", Nothing);
+    caml_win32_maperr(GetLastError());
+    caml_uerror("set_console_cp", Nothing);
   }
-  return (Val_unit);
+  CAMLreturn(Val_unit);
+}
+
+CAMLprim value win_vt_capable(value fd)
+{
+  CAMLparam1(fd);
+  DWORD mode;
+
+  if (Handle_val(fd) == INVALID_HANDLE_VALUE) {
+    CAMLreturn(Val_int(0));
+  }
+
+  if (!GetConsoleMode(Handle_val(fd), &mode)) {
+    CAMLreturn(Val_int(0));
+  }
+
+  CAMLreturn(Val_int(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING));
 }
diff --git a/src/system/win/system_impl.ml b/src/system/win/system_impl.ml
index f03bae0..333be5c 100644
--- a/src/system/win/system_impl.ml
+++ b/src/system/win/system_impl.ml
@@ -15,51 +15,4 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *)
 
-module System = System_win.M (struct let useLongUNCPaths = false end)
-
-module Fs = struct
-
-  let unicode = ref false
-
-  let setUnicodeEncoding u = unicode := u
-
-  let c1 f1 f2 v1 = if !unicode then f1 v1 else f2 v1
-  let c2 f1 f2 v1 v2 = if !unicode then f1 v1 v2 else f2 v1 v2
-  let c3 f1 f2 v1 v2 v3 = if !unicode then f1 v1 v2 v3 else f2 v1 v2 v3
-
-  module G = System_generic
-  module W = System_win.M (struct let useLongUNCPaths = true end)
-
-  type fspath = string
-
-  let fspathConcat v1 v2 = c2 W.fspathConcat G.fspathConcat v1 v2
-  let fspathDirname v = c1 W.fspathDirname G.fspathDirname v
-
-  type dir_handle = G.dir_handle
-                  = { readdir : unit -> string; closedir : unit -> unit }
-
-  let symlink v1 v2 = c2 W.symlink G.symlink v1 v2
-  let readlink v = c1 W.readlink G.readlink v
-  let chown v1 v2 v3 = c3 W.chown G.chown v1 v2 v3
-  let chmod v1 v2 = c2 W.chmod G.chmod v1 v2
-  let utimes v1 v2 v3 = c3 W.utimes G.utimes v1 v2 v3
-  let unlink v = c1 W.unlink G.unlink v
-  let rmdir v = c1 W.rmdir G.rmdir v
-  let mkdir v1 v2 = c2 W.mkdir G.mkdir v1 v2
-  let rename v1 v2 = c2 W.rename G.rename v1 v2
-  let stat v = c1 W.stat G.stat v
-  let lstat v = c1 W.lstat G.lstat v
-  let opendir v = c1 W.opendir G.opendir v
-  let openfile v1 v2 v3 = c3 W.openfile G.openfile v1 v2 v3
-  let open_in_gen v1 v2 v3 = c3 W.open_in_gen G.open_in_gen v1 v2 v3
-  let open_out_gen v1 v2 v3 = c3 W.open_out_gen G.open_out_gen v1 v2 v3
-  let getcwd v = c1 W.getcwd G.getcwd v
-  let chdir v = c1 W.chdir G.chdir v
-  let readlink v = c1 W.readlink G.readlink v
-  let fingerprint v = c1 W.fingerprint G.fingerprint v
-
-  let canSetTime v = c1 W.canSetTime G.canSetTime v
-  let hasInodeNumbers v = c1 W.hasInodeNumbers G.hasInodeNumbers v
-  let hasSymlink v = c1 W.hasSymlink G.hasSymlink v
-  let hasCorrectCTime = if !unicode then W.hasCorrectCTime else G.hasCorrectCTime
-end
+include System_win
diff --git a/src/terminal.ml b/src/terminal.ml
index a960808..2618ca5 100644
--- a/src/terminal.ml
+++ b/src/terminal.ml
@@ -62,89 +62,6 @@ let authenticity s = Rx.match_string authenticityRx s
    SSH password interaction.
 *)
 
-(*
-let a1 = [|'p';'q';'r';'s';'t';'u';'v';'w';'x';'y';'z';'P';'Q';'R';'S';'T'|]
-let a2 = [|'0';'1';'2';'3';'4';'5';'6';'7';'8';'9';'a';'b';'c';'d';'e';'f'|]
-exception Break of (Unix.file_descr * string) option
-let ptyMasterOpen () =
-  if not(Osx.isMacOSX or Osx.isLinux) then None else
-  try
-    (* Adapted from Stevens' Advanced Programming in Unix *)
-    let x = "/dev/pty--" in
-    for i = 0 to Array.length a1 do
-      x.[8] <- a1.(i);
-      for j = 0 to Array.length a2 do
-        x.[9] <- a2.(j);
-        let fdOpt =
-          try Some(Unix.openfile x [Unix.O_RDWR] 0)
-          with Unix.Unix_error _ -> None in
-        match fdOpt with None -> ()
-        | Some fdMaster ->
-          x.[5] <- 't';
-          raise (Break(Some(fdMaster,x)))
-      done
-    done;
-    None
-  with Break z -> z
-
-let ptySlaveOpen = function
-    None -> None
-  | Some(fdMaster,ttySlave) ->
-      let slave =
-        try Some (Unix.openfile ttySlave [Unix.O_RDWR] 0o600)
-        with Unix.Unix_error _ -> None in
-      (try Unix.close fdMaster with Unix.Unix_error(_,_,_) -> ());
-      slave
-
-let printTermAttrs fd = (* for debugging *)
-  let tio = Unix.tcgetattr fd in
-  let boolPrint name x d =
-    if x then Printf.printf "%s is ON (%s)\n" name d
-    else Printf.printf "%s is OFF (%s)\n" name d in
-  let intPrint name x d =
-    Printf.printf "%s = %d (%s)\n" name x d in
-  let charPrint name x d =
-    Printf.printf "%s = '%c' (%s)\n" name x d in
-  boolPrint "c_ignbrk" tio.Unix.c_ignbrk   "Ignore the break condition.";
-  boolPrint "c_brkint" tio.Unix.c_brkint   "Signal interrupt on break condition.";
-  boolPrint "c_ignpar" tio.Unix.c_ignpar   "Ignore characters with parity errors.";
-  boolPrint "c_parmrk" tio.Unix.c_parmrk   "Mark parity errors.";
-  boolPrint "c_inpck" tio.Unix.c_inpck     "Enable parity check on input.";
-  boolPrint "c_istrip" tio.Unix.c_istrip   "Strip 8th bit on input characters.";
-  boolPrint "c_inlcr" tio.Unix.c_inlcr     "Map NL to CR on input.";
-  boolPrint "c_igncr" tio.Unix.c_igncr     "Ignore CR on input.";
-  boolPrint "c_icrnl" tio.Unix.c_icrnl     "Map CR to NL on input.";
-  boolPrint "c_ixon" tio.Unix.c_ixon       "Recognize XON/XOFF characters on input.";
-  boolPrint "c_ixoff" tio.Unix.c_ixoff     "Emit XON/XOFF chars to control input flow.";
-  boolPrint "c_opost" tio.Unix.c_opost     "Enable output processing.";
-  intPrint "c_obaud" tio.Unix.c_obaud      "Output baud rate (0 means close connection).";
-  intPrint "c_ibaud" tio.Unix.c_ibaud      "Input baud rate.";
-  intPrint "c_csize" tio.Unix.c_csize      "Number of bits per character (5-8).";
-  intPrint "c_cstopb" tio.Unix.c_cstopb    "Number of stop bits (1-2).";
-  boolPrint "c_cread" tio.Unix.c_cread     "Reception is enabled.";
-  boolPrint "c_parenb" tio.Unix.c_parenb   "Enable parity generation and detection.";
-  boolPrint "c_parodd" tio.Unix.c_parodd   "Specify odd parity instead of even.";
-  boolPrint "c_hupcl" tio.Unix.c_hupcl     "Hang up on last close.";
-  boolPrint "c_clocal" tio.Unix.c_clocal   "Ignore modem status lines.";
-  boolPrint "c_isig" tio.Unix.c_isig       "Generate signal on INTR, QUIT, SUSP.";
-  boolPrint "c_icanon" tio.Unix.c_icanon   "Enable canonical processing (line buffering and editing)";
-  boolPrint "c_noflsh" tio.Unix.c_noflsh   "Disable flush after INTR, QUIT, SUSP.";
-  boolPrint "c_echo" tio.Unix.c_echo       "Echo input characters.";
-  boolPrint "c_echoe" tio.Unix.c_echoe     "Echo ERASE (to erase previous character).";
-  boolPrint "c_echok" tio.Unix.c_echok     "Echo KILL (to erase the current line).";
-  boolPrint "c_echonl" tio.Unix.c_echonl   "Echo NL even if c_echo is not set.";
-  charPrint "c_vintr" tio.Unix.c_vintr     "Interrupt character (usually ctrl-C).";
-  charPrint "c_vquit" tio.Unix.c_vquit     "Quit character (usually ctrl-\\).";
-  charPrint "c_verase" tio.Unix.c_verase   "Erase character (usually DEL or ctrl-H).";
-  charPrint "c_vkill" tio.Unix.c_vkill     "Kill line character (usually ctrl-U).";
-  charPrint "c_veof" tio.Unix.c_veof       "End-of-file character (usually ctrl-D).";
-  charPrint "c_veol" tio.Unix.c_veol       "Alternate end-of-line char. (usually none).";
-  intPrint "c_vmin" tio.Unix.c_vmin        "Minimum number of characters to read before the read request is satisfied.";
-  intPrint "c_vtime" tio.Unix.c_vtime      "Maximum read wait (in 0.1s units).";
-  charPrint "c_vstart" tio.Unix.c_vstart   "Start character (usually ctrl-Q).";
-  charPrint "c_vstop" tio.Unix.c_vstop      "Stop character (usually ctrl-S)."
-*)
-
 (* Implemented in file pty.c *)
 type pty
 external win_openpty : unit -> (Unix.file_descr * Unix.file_descr)
@@ -160,26 +77,57 @@ external c_openpty : unit -> Unix.file_descr * Unix.file_descr =
 let openpty() = try Some (c_openpty ()) with Unix.Unix_error _ -> None
 
 (* Utility functions copied from ocaml's unix.ml because they are not exported :-| *)
-let rec safe_dup fd =
-  let new_fd = Unix.dup fd in
-  if dumpFd new_fd >= 3 then
-    new_fd
-  else begin
-    let res = safe_dup fd in
-    Unix.close new_fd;
-    res
-  end
+(* Duplicate [fd] if needed to make sure it isn't one of the
+   standard descriptors (stdin, stdout, stderr).
+   Note that this function always leaves the standard descriptors open,
+   the caller must take care of closing them if needed.
+   The "cloexec" mode doesn't matter, because
+   the descriptor returned by [dup] will be closed before the [exec],
+   and because no other thread is running concurrently
+   (we are in the child process of a fork).
+ *)
+let rec file_descr_not_standard fd =
+  if dumpFd fd >= 3 then fd else file_descr_not_standard (Unix.dup fd)
+
 let safe_close fd = try Unix.close fd with Unix.Unix_error _ -> ()
+
 let perform_redirections new_stdin new_stdout new_stderr =
-  let newnewstdin = safe_dup new_stdin in
-  let newnewstdout = safe_dup new_stdout in
-  let newnewstderr = safe_dup new_stderr in
+  let new_stdin = file_descr_not_standard new_stdin in
+  let new_stdout = file_descr_not_standard new_stdout in
+  let new_stderr = file_descr_not_standard new_stderr in
+  (*  The three dup2 close the original stdin, stdout, stderr,
+      which are the descriptors possibly left open
+      by file_descr_not_standard *)
+  Unix.dup2 ~cloexec:false new_stdin Unix.stdin;
+  Unix.dup2 ~cloexec:false new_stdout Unix.stdout;
+  Unix.dup2 ~cloexec:false new_stderr Unix.stderr;
   safe_close new_stdin;
   safe_close new_stdout;
-  safe_close new_stderr;
-  Unix.dup2 newnewstdin Unix.stdin; Unix.close newnewstdin;
-  Unix.dup2 newnewstdout Unix.stdout; Unix.close newnewstdout;
-  Unix.dup2 newnewstderr Unix.stderr; Unix.close newnewstderr
+  safe_close new_stderr
+
+let rec safe_waitpid pid =
+  (* This function is intentionally synchronous so that it can be run during
+     cleanup code when Lwt threads might be stopped or otherwise be in an
+     unreliable state. *)
+  let kill_noerr si = try Unix.kill pid si with Unix.Unix_error _ -> () in
+  let t = Unix.gettimeofday () in
+  let rec aux st =
+    match Unix.waitpid [Unix.WNOHANG] pid with
+    | (0, _) ->
+        Unix.sleepf 0.002;
+        let dt = Unix.gettimeofday () -. t in
+        if dt >= 0.5 && st = 0 then begin
+          kill_noerr Sys.(if os_type = "Win32" then sigkill else sigterm);
+          aux 1
+        end else if dt >= 2.0 && st = 1 then begin
+          kill_noerr Sys.sigkill;
+          aux 2
+        end else
+          aux st
+    | (_, r) -> r
+    | exception Unix.Unix_error (EINTR, _, _) -> aux st
+  in
+  aux 0
 
 let term_sessions = Hashtbl.create 3
 
@@ -208,8 +156,38 @@ let finally f g =
     begin try g () with Sys_error _  | Unix.Unix_error _ -> () end;
     raise e
 
+external win_alloc_console : unit -> Unix.file_descr option = "win_alloc_console"
+
 let fallback_session cmd args new_stdin new_stdout new_stderr =
-  (None, System.create_process cmd args new_stdin new_stdout new_stderr)
+  if Sys.os_type = "Win32" then begin
+    (* OCaml's [Unix.create_process] hides the Windows console window of
+       the child process unless the parent process already has a console.
+       This is unsuitable for running interactive child processes like
+       the ssh client. To make it possible to use the ssh client without pty,
+       we have to open a Windows console window before launching the child
+       process. Unfortunately, we can't know if the ssh client (or any other
+       remote shell client) requires user interaction via the Windows console
+       or not.
+
+       Ignore any errors because it is almost certain that the error indicates
+       that a console already exists (and we can't do anything about other
+       errors anyway).
+
+       If a new console was allocated and [Unix.stderr] is invalid (which
+       will happen in Windows for GUI without a console unless stderr is
+       redirected elsewhere; this is checked in the C stub) then also
+       redirect [Unix.stderr] to the new console. [new_stderr] is most likely
+       [Unix.stderr] and will therefore be associated with the new console. *)
+    try
+      match win_alloc_console () with
+      | None -> ()
+      | Some fd -> try Unix.dup2 fd Unix.stderr with Unix.Unix_error _ -> ()
+    with Unix.Unix_error _ -> ()
+  end;
+  let childPid =
+    System.create_process cmd args new_stdin new_stdout new_stderr in
+  Hashtbl.add term_sessions childPid (fun () -> ignore (safe_waitpid childPid));
+  (None, childPid)
 
 let win_create_session cmd args new_stdin new_stdout new_stderr =
   match win_openpty () with
@@ -227,7 +205,7 @@ let win_create_session cmd args new_stdin new_stdout new_stderr =
       let fdIn = Lwt_unix.of_unix_file_descr masterIn
       and fdOut = Lwt_unix.of_unix_file_descr masterOut in
       let ret = Some (fdIn, fdOut) in
-      Hashtbl.add term_sessions ret
+      Hashtbl.add term_sessions childPid
         (fun () -> finally (fun () -> win_closepty pty)
                            (fun () -> finally (fun () -> Lwt_unix.close fdOut)
                                               (fun () -> Lwt_unix.close fdIn)));
@@ -241,14 +219,19 @@ let unix_create_session cmd args new_stdin new_stdout new_stderr =
   match openpty () with
     None -> fallback_session cmd args new_stdin new_stdout new_stderr
   | Some (masterFd, slaveFd) ->
-(*
-      Printf.printf "openpty returns %d--%d\n" (dumpFd fdM) (dumpFd fdS); flush stdout;
-      Printf.printf "new_stdin=%d, new_stdout=%d, new_stderr=%d\n"
-        (dumpFd new_stdin) (dumpFd new_stdout) (dumpFd new_stderr) ; flush stdout;
-*)
+      Unix.set_close_on_exec masterFd;
+      Unix.set_close_on_exec slaveFd;
+      flush_all (); (* Clear buffers to avoid risk of double flushing by child.
+        Even this is not sufficient, strictly speaking, as there is a window
+        of opportunity to fill the buffer between flushing and calling fork. *)
       begin match Unix.fork () with
         0 ->
           begin try
+            (* Child process stderr must redirected as early as possible to
+               make sure all error output is captured and visible in GUI. *)
+            Unix.dup2 ~cloexec:false slaveFd Unix.stderr;
+            (* new_stderr will be used by parent process only. *)
+            if new_stderr <> Unix.stderr then safe_close new_stderr;
             Unix.close masterFd;
             ignore (Unix.setsid ());
             setControllingTerminal slaveFd;
@@ -256,11 +239,23 @@ let unix_create_session cmd args new_stdin new_stdout new_stderr =
             let tio = Unix.tcgetattr slaveFd in
             tio.Unix.c_echo <- false;
             Unix.tcsetattr slaveFd Unix.TCSANOW tio;
+            (* Redirect ssh authentication errors to controlling terminal,
+               instead of new_stderr, so that they can be captured by GUI.
+               This will inevitably also redirect the remote stderr to GUI
+               as ssh's own error output is mixed with remote stderr output. *)
             perform_redirections new_stdin new_stdout slaveFd;
             Unix.execvp cmd args (* never returns *)
-          with Unix.Unix_error _ ->
-            Printf.eprintf "Some error in create_session child\n";
+          with Unix.Unix_error (e, s1, s2) ->
+            Printf.eprintf "Error in create_session child: [%s] (%s) %s\n"
+              s1 s2 (Unix.error_message e);
             flush stderr;
+            (* FIXME: this should be Unix._exit (available from OCaml 4.12)
+               which doesn't flush buffers (or run other exit handlers).
+               When [_exit] is eventually used then to _completely_ avoid risk
+               of double flushing, [Unix.write Unix.stderr] should be used
+               above instead of [eprintf]. Using [_exit] and not using any
+               [Stdlib.out_channel] will avoid all buffering and exit handler
+               issues. *)
             exit 127
           end
       | childPid ->
@@ -270,9 +265,10 @@ let unix_create_session cmd args new_stdin new_stdout new_stderr =
           (* Unix.close slaveFd; *)
           let fd = Lwt_unix.of_unix_file_descr masterFd in
           let ret = Some (fd, fd) in
-          Hashtbl.add term_sessions ret
+          Hashtbl.add term_sessions childPid
             (fun () -> safe_close slaveFd;
-                       Lwt_unix.close fd);
+                       finally (fun () -> Lwt_unix.close fd)
+                               (fun () -> ignore (safe_waitpid childPid)));
           (ret, childPid)
       end
 
@@ -281,36 +277,124 @@ let create_session =
   | "Win32" -> win_create_session
   | _       -> unix_create_session
 
-let close_session = function
-  | None -> ()
-  | Some _ as fdopt ->
-      try
-        let cleanup = Hashtbl.find term_sessions fdopt in
-        Hashtbl.remove term_sessions fdopt;
-        cleanup ()
-      with Not_found ->
-        raise (Unix.Unix_error (Unix.EBADF, "Terminal.close_session", ""))
+let close_session pid =
+  try
+    let cleanup = Hashtbl.find term_sessions pid in
+    Hashtbl.remove term_sessions pid;
+    cleanup ()
+  with Not_found ->
+    raise (Unix.Unix_error (Unix.ESRCH, "Terminal.close_session", ""))
 
 let (>>=) = Lwt.bind
 
-let escRemove = Str.regexp
-   ("\\(\\(.\\|[\n\r]\\)+\027\\[[12]J\\)" (* Clear screen *)
-  ^ "\\|\\(\027\\[[0-2]?J\\)" (* Clear screen *)
-  ^ "\\|\\(\027\\[!p\\)" (* Soft reset *)
-  ^ "\\|\\(\027\\][02];[^\007]*\007\\)" (* Set console window title *)
-  ^ "\\|\\(\027\\[\\?25[hl]\\)" (* Show/hide cursor *)
-  ^ "\\|\\(\027\\[[0-9;]*m\\)" (* Formatting *)
-  ^ "\\|\\(\027\\[H\\)") (* Home *)
-
-let escSpace = Str.regexp "\027\\[\\([0-9]*\\)C"
+(* OpenSSH on Windows is known to produce at least the following escape
+   sequences. Examples of raw output with OCaml string escapes, starting from
+   beginning of line and ending at end of line, newline excluded:
+
+\027[2J\027[m\027[H\027]0;C:\\WINDOWS\\System32\\OpenSSH\\ssh.exe\007\027[?25h
+
+The authenticity of host 'example.com (127.0.0.1)' can't be established.\r\nECDSA key fingerprint is SHA256:CxGGHIVL7YDoSAtAzkIJNNaheGW7dDa7m7H+antMzDv.  \r\nAre you sure you want to continue connecting (yes/no/[fingerprint])?\027[10X\027[1C
+
+   Most of these sequences are clearly useless for Unison and can be safely
+   ignored. The final sequence CSI 10 X CSI 1 C is a bit weird. In this
+   context, CSI 1 C can be interpreted as 1 space, although this is not
+   universal.
+
+   Some versions may have also emitted CSI ! p (VT220 soft reset) but this
+   no longer seems to be the case. *)
+
+type controlSt = No | Escape | EscapeSeq | CSI | OSC | StringSeq | OSCEsc | StringEsc
+
+(* A very primitive and minimal parser of ANSI X3.64/ECMA-48 control sequences.
+   It parses 7-bit control characters (C0) only. 8-bit control characters (C1)
+   are intentionally not parsed.
+   The vast majority of sequences are just ignored. *)
+let parseCtrlSeq s =
+  let s' = Buffer.create (String.length s) in
+  let add_char = Buffer.add_char s' in
+  let params = Buffer.create 32 in
+  let params_add_char = Buffer.add_char params in
+  let st = ref No in
+  let state x = st := x in
+  let parseEsc ch =
+    Buffer.clear params;
+    match ch with
+    | '\032'..'\047' -> state EscapeSeq
+    | '[' -> state CSI
+    | ']' -> state OSC
+    | 'X' | '^' | '_' -> state StringSeq
+    | _ -> state No
+  in
+  let parseCh ch =
+    match !st with
+    | No when ch = '\027' -> state Escape
+    | No -> add_char ch
+    | Escape -> parseEsc ch
+    | EscapeSeq ->
+        begin
+          match ch with
+          | '\024' | '\026' -> state No (* CAN, SUB *)
+          | '\000'..'\025' -> add_char ch (* Control charaters (roughly) *)
+          | '\027' -> state Escape
+          | '\048'..'\126' -> state No (* Final *)
+          | '\127'..'\255' -> state No (* Invalid *)
+          | _ -> ()
+        end
+    | CSI ->
+        begin
+          match ch with
+          | '\024' | '\026' -> state No (* CAN, SUB *)
+          | '\000'..'\025' -> add_char ch (* Control charaters (roughly) *)
+          | '\027' -> state Escape
+          | '\064'..'\126' -> (* Final *)
+              begin
+                state No;
+                match ch with
+                | 'C' -> (* cursor forward *)
+                    let n =
+                      try int_of_string (Buffer.contents params)
+                      with Failure _ -> 1 in
+                    for _ = 1 to n do add_char ' ' done
+                | _ -> ()
+              end
+          | '\127'..'\255' -> state No (* Invalid *)
+          | _ -> params_add_char ch
+        end
+    | OSC ->
+        begin
+          match ch with
+          | '\024' | '\026' -> state No (* CAN, SUB *)
+          | '\007' -> state No (* BEL *)
+          | '\000'..'\025' -> add_char ch (* Control charaters (roughly) *)
+          | '\027' -> state OSCEsc
+          | _ -> ()
+        end
+    | OSCEsc ->
+        begin
+          match ch with
+          | '\\' -> state No (* String terminator *)
+          | _ -> parseEsc ch
+        end
+    | StringSeq ->
+        begin
+          match ch with
+          | '\024' | '\026' -> state No (* CAN, SUB *)
+          | '\000'..'\025' -> add_char ch (* Control charaters (roughly) *)
+          | '\027' -> state StringEsc
+          | _ -> ()
+        end
+    | StringEsc ->
+        begin
+          match ch with
+          | '\\' -> state No (* String terminator *)
+          | _ -> parseEsc ch
+        end
+  in
+  String.iter parseCh s;
+  Buffer.contents s'
 
 let processEscapes s =
-  let whitesp s =
-    try String.make (min 1 (int_of_string (Str.replace_matched "\\1" s))) ' '
-    with Failure _ -> " "
-  in
-  Str.global_replace escRemove "" s
-  |> Str.global_substitute escSpace whitesp
+  parseCtrlSeq s
 
 (* Wait until there is input. If there is terminal input s,
    return Some s. Otherwise, return None. *)
@@ -335,26 +419,80 @@ let rec termInput (fdTerm, _) fdInput =
     (Lwt.choose
        [readPrompt (); connectionEstablished ()])
 
+type termInteract = {
+  userInput : string -> (string -> unit) -> unit;
+  endInput : unit -> unit }
+
 (* Read messages from the terminal and use the callback to get an answer *)
-let handlePasswordRequests (fdIn, fdOut) callback =
-  let buf = Bytes.create 10000 in
-  let rec loop () =
-    Lwt_unix.read fdIn buf 0 10000 >>= (fun len ->
-      if len = 0 then
-        (* The remote end is dead *)
-        Lwt.return ()
+let handlePasswordRequests (fdIn, fdOut) {userInput; endInput} isReady =
+  let scrollback = Buffer.create 32 in
+  let extract () =
+    let s = Buffer.contents scrollback in
+    let () = Buffer.clear scrollback in
+    s
+  in
+  let blen = 10000 in
+  let buf = Bytes.create blen in
+  let ended = ref false in
+  let closeInput () =
+    ended := true;
+    endInput ()
+  in
+  let terminalError loc e =
+    closeInput ();
+    Util.encodeException loc `Fatal e
+  in
+  let sendResponse s =
+    Lwt.catch
+      (fun () ->
+        if isReady () || !ended then Lwt.return 0
+        else Lwt_unix.write_substring fdOut (s ^ "\n") 0 (String.length s + 1))
+      (terminalError "writing to shell terminal")
+  in
+  let promptUser () =
+    let query = extract () in
+    if query = "\r\n" || query = "\n" || query = "\r" then ()
+    else
+      (* There is a tiny, almost non-existent risk of a broken escape sequence
+         at the very beginning or the very end of the buffer (this can happen
+         if bytes read from the pty end in the middle of a sequence and before
+         reading any further we charge ahead with processing what we've read).
+         Given that it's almost certainly ssh we're dealing with, this risk can
+         safely be ignored. *)
+      let querytext = processEscapes query in
+      if querytext = "" || String.trim querytext = "" then ()
       else
-        let query = Bytes.sub_string buf 0 len in
-        if query = "\r\n" || query = "\n" || query = "\r" then
+        userInput querytext (fun s -> Lwt.ignore_result (sendResponse s))
+  in
+  let rec loop () =
+    (* When reading from a pty, the reading loop will not stop even when the
+       remote shell process dies. The reading will end (return 0 or an error)
+       when the pty is closed.
+       The only way to stop the reading loop without closing the pty is to
+       signal [isReady]. *)
+    Lwt.catch
+      (fun () -> Lwt_unix.read fdIn buf 0 blen)
+      (fun ex -> if isReady () || !ended then Lwt.return 0 else Lwt.fail ex)
+    >>= function
+    | 0 -> Lwt.return ()
+    | len ->
+        Buffer.add_string scrollback (Bytes.sub_string buf 0 len);
+        if isReady () then begin (* The shell connection has been established *)
+          closeInput ();
+          Lwt.return ()
+        end else begin
+          Lwt.ignore_result (Lwt_unix.sleep 0.05 >>= fun () -> (* Give time for connection checks *)
+            Lwt.return (if not !ended && not (isReady ()) then promptUser ()));
           loop ()
-        else begin
-          let response = callback (processEscapes query) in
-          Lwt_unix.write_substring fdOut
-            (response ^ "\n") 0 (String.length response + 1)
-            (* HACK: Sleep briefly to allow time for output to be
-               generated and read in as a whole. *)
-              >>= fun _ -> Lwt_unix.sleep 0.2 >>= (fun _ ->
-          loop ())
-        end)
+        end
+  in
+  let readTerm = Lwt.catch loop (terminalError "reading from shell terminal") in
+  let extractRemainingOutput clean =
+    closeInput ();
+    (* Give a final chance of reading the error output from the ssh process. *)
+    let timeout = Lwt_unix.sleep 0.3 in
+    Lwt.choose [readTerm; timeout] >>= fun () ->
+    if not clean then Lwt.return (extract ())
+    else Lwt.return (Util.trimWhitespace (processEscapes (extract ())))
   in
-  ignore (loop ())
+  (readTerm, extractRemainingOutput)
diff --git a/src/terminal.mli b/src/terminal.mli
index dfa83e3..369300c 100644
--- a/src/terminal.mli
+++ b/src/terminal.mli
@@ -7,8 +7,13 @@ val create_session :
   Unix.file_descr -> Unix.file_descr -> Unix.file_descr ->
   (Lwt_unix.file_descr * Lwt_unix.file_descr) option * int
 
-val close_session :
-  (Lwt_unix.file_descr * Lwt_unix.file_descr) option -> unit
+val close_session : int -> unit
+
+(* [safe_waitpid] is intended for waiting on child processes that are
+   expected to terminate by themselves. If the child process has not
+   terminated after a short while then a SIGTERM is sent and if the
+   child process still doesn't terminate then a SIGKILL is sent. *)
+val safe_waitpid : int -> Unix.process_status
 
 (* termInput fdTerm fdInput
    Wait until there is input on at least one file descriptor.
@@ -17,8 +22,13 @@ val close_session :
 val termInput :
   (Lwt_unix.file_descr * Lwt_unix.file_descr) -> Lwt_unix.file_descr -> string option
 
+type termInteract = {
+  userInput : string -> (string -> unit) -> unit;
+  endInput : unit -> unit }
+
 val handlePasswordRequests :
-  (Lwt_unix.file_descr * Lwt_unix.file_descr) -> (string -> string) -> unit
+  (Lwt_unix.file_descr * Lwt_unix.file_descr) -> termInteract ->
+  (unit -> bool) -> unit Lwt.t * (bool -> string Lwt.t)
 
 (* For recognizing messages from OpenSSH *)
 val password : string -> bool
diff --git a/src/test.ml b/src/test.ml
index 784295e..c183101 100644
--- a/src/test.ml
+++ b/src/test.ml
@@ -104,6 +104,18 @@ type fs =
   | Link of string
   | Dir of (string * fs) list
 
+let mfs_rec fs = Umarshal.(sum3 string string (list (prod2 string fs id id))
+                             (function
+                              | File a -> I31 a
+                              | Link a -> I32 a
+                              | Dir a -> I33 a)
+                             (function
+                              | I31 a -> File a
+                              | I32 a -> Link a
+                              | I33 a -> Dir a))
+
+let mfs = Umarshal.rec1 mfs_rec
+
 let rec equal fs1 fs2 =
   match fs1,fs2 with
     | File s1, File s2 -> s1=s2
@@ -157,7 +169,7 @@ let writefs p fs =
 
 let checkRootEmpty : Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "checkRootEmpty"
+    "checkRootEmpty" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) ->
        if Os.exists fspath Path.empty then
          raise (Util.Fatal (Printf.sprintf
@@ -167,26 +179,26 @@ let checkRootEmpty : Common.root -> unit -> unit Lwt.t =
 
 let makeRootEmpty : Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "makeRootEmpty"
+    "makeRootEmpty" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) ->
        remove_file_or_dir fspath;
        Lwt.return ())
 
 let getfs : Common.root -> unit -> (fs option) Lwt.t =
   Remote.registerRootCmd
-    "getfs"
+    "getfs" Umarshal.unit Umarshal.(option mfs)
     (fun (fspath, ()) ->
        Lwt.return (readfs fspath))
 
 let getbackup : Common.root -> unit -> (fs option) Lwt.t =
   Remote.registerRootCmd
-    "getbackup"
+    "getbackup" Umarshal.unit Umarshal.(option mfs)
     (fun (fspath, ()) ->
        Lwt.return (readfs (Stasher.backupDirectory ())))
 
 let makeBackupEmpty : Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "makeBackupEmpty"
+    "makeBackupEmpty" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) ->
        let b = Stasher.backupDirectory () in
        debug (fun () -> Util.msg "Removing %s\n" (Fspath.toDebugString b));
@@ -194,7 +206,7 @@ let makeBackupEmpty : Common.root -> unit -> unit Lwt.t =
 
 let putfs : Common.root -> fs -> unit Lwt.t =
   Remote.registerRootCmd
-    "putfs"
+    "putfs" mfs Umarshal.unit
     (fun (fspath, fs) ->
        writefs fspath fs;
        Lwt.return ())
@@ -223,12 +235,9 @@ let sync ?(verbose=false) () =
     displayRis reconItemList
   end;
   minisleep 0.1;
-  Lwt_unix.run (
-    Lwt_util.iter
-      (fun ri ->
-         Transport.transportItem ri
-           (Uutil.File.ofLine 0) (fun _ _ -> true))
-      reconItemList);
+  Uicommon.transportItems (Array.of_list reconItemList) (fun _ -> true)
+    (fun _ ri ->
+      Transport.transportItem ri (Uutil.File.ofLine 0) (fun _ _ -> true));
   Update.commitUpdates()
 
 let currentTest = ref ""
@@ -244,13 +253,13 @@ let test() =
   Prefs.set Trace.terse true;
   Trace.sendLogMsgsToStderr := false;
 
-  let origPrefs = Prefs.dump() in
+  let origPrefs = Prefs.dump 99 in
 
   let runtest name prefs f =
     Util.msg "%s...\n" name;
     Util.convertUnixErrorsToFatal "Test.test" (fun() ->
       currentTest := name;
-      Prefs.load origPrefs;
+      Prefs.load origPrefs 99;
       loadPrefs prefs;
       debug (fun() -> Util.msg "Emptying backup directory\n");
       Lwt_unix.run (Globals.allRootsIter (fun r -> makeBackupEmpty r ()));
@@ -326,6 +335,22 @@ let test() =
      running fast enough that the whole thing happens within a second, then the
      update will be missed! *)
 
+  (* Test that update propagation transport works *)
+  let maxth = [| "0"; "1"; "5"; "6"; "7" |] in
+    (* Number of threads: default (0); 1 (corner case);
+       one less, equal to, and one more than number of updates *)
+  for i = 1 to Array.length maxth do
+    runtest ("propagation 1." ^ string_of_int i) ["maxthreads = " ^ maxth.(i - 1)] (fun () ->
+      put R1 (Dir []); put R2 (Dir []); sync ();
+      let r1 = ["a", File "a"; "b", File "b"; "d1", Dir ["a", File "a1"; "b", File "b1"]]
+      and r2 = ["x", File "x"; "y", File "y"; "d2", Dir ["x", File "x2"; "y", File "y2"]] in
+      let expect = Dir (r1 @ r2) in
+      put R1 (Dir r1); put R2 (Dir r2); sync ();
+      check "1" R1 expect;
+      check "2" R2 expect
+    )
+  done;
+
   (* Test that .git is treated atomically. *)
   runtest "Atomicity of certain directories 1" ["atomic = Name .git";
                                                 "force = newer"] (fun() ->
@@ -399,7 +424,6 @@ let test() =
       put R1 (Dir ["x", File "foo"]);
       put R2 (Dir ["x", File "bar"]); sync();
       (* Change contents without changing size and check that change is propagated *)
-      Unix.sleep 2; (* in case time granularity is coarse on this FS *)
       put R1 (Dir ["x", File "f00"]); sync();
 
       check "3a" R1 (Dir ["x", File "f00"]);
@@ -443,7 +467,7 @@ let test() =
     (* Create a file and a directory *)
     put R1 (Dir ["foo", File "1"]); sync();
     check "1" R1 (Dir [("foo", File "1")]);
-    check "2" R1 (Dir [("foo", File "1")]);
+    check "2" R2 (Dir [("foo", File "1")]);
     put R1 (Dir ["foo", File "2"]); sync();
     check "3" R1 (Dir [("foo", File "2")]);
     check "4" R2 (Dir [("foo", File "2"); (".bak.0.foo", File "1")]);
@@ -543,6 +567,77 @@ let test() =
     *)
   end;
 
+  if not bothRootsLocal then
+  begin
+    let localR, remoteR, localRaw =
+      match r1 with
+      | Common.Local, _ -> R1, R2, r1
+      | _ -> R2, R1, r2
+    in
+
+    (* Test RPC function "fingerprintSubfile" *)
+    runtest "RPC: transfer append" [] (fun () ->
+      let prefixLen = 1024 * 1024 + 1 in
+      let len = prefixLen + 31 in
+      let contents = String.make len '.' in
+      let fileName = "bigfile" in
+      let prefixPath = Path.fromString fileName in
+      let (workingDir, _) = Fspath.findWorkingDir (snd localRaw) prefixPath in
+      let prefixName = Path.toString (Os.tempPath ~fresh:false workingDir prefixPath) in
+      put remoteR (Dir [(fileName, File contents)]);
+      put localR (Dir [(prefixName, File (String.sub contents 0 prefixLen))]);
+      sync ();
+      check "1" localR (Dir [(fileName, File contents)]);
+    );
+
+    (* Test RPC function "updateProps" *)
+    runtest "RPC: update props" ["times = true"] (fun () ->
+      let state = [("a", File "x")] in
+      put remoteR (Dir state);
+      put localR (Dir []);
+      sync ();
+      (* Having to sleep here is an unfortunate side-effect of the current
+         Windows limitations-inspired time comparison algorithm which is
+         designed to work on FAT filesystems (2-second granularity). *)
+      Unix.sleep 2;
+      put remoteR (Dir state);
+      sync ();
+      check "1" localR (Dir state);
+    );
+
+    (* Test RPC function "replaceArchive" *)
+    runtest "RPC: replaceArchive" [] (fun () ->
+      put localR (Dir [("n", File "to delete")]);
+      put remoteR (Dir []);
+      sync ();
+      put remoteR (Dir []);
+      sync ();
+      check "1" localR (Dir []);
+    );
+
+    (* Test RPC functions "mkdir" and "setDirProp" *)
+    runtest "RPC: mkdir, setDirProp" [] (fun () ->
+      let state = [("subd", Dir [])] in
+      put localR (Dir state);
+      put remoteR (Dir []);
+      sync ();
+      check "1" remoteR (Dir state);
+    );
+
+    (* Test RPC function "setupTargetPaths" *)
+    runtest "RPC: merge" ["merge = Name ma -> echo x> NEW"; "backupcurr = Name ma"] (fun () ->
+      let result = match Sys.os_type with
+        | "Win32" -> ("ma", File "x\r\n")
+        | _ -> ("ma", File "x\n")
+      in
+      put localR (Dir [("ma", File "a")]);
+      put remoteR (Dir [("ma", File "b")]);
+      sync ();
+      check "1" localR (Dir [result]);
+      check "2" remoteR (Dir [result]);
+    );
+  end;
+
   if !failures = 0 then
     Util.msg "Success :-)\n"
   else
diff --git a/src/transfer.ml b/src/transfer.ml
index 07fe4b7..0719f96 100644
--- a/src/transfer.ml
+++ b/src/transfer.ml
@@ -333,6 +333,13 @@ struct
         (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t;
       strongChecksum : Bytearray.t }
 
+  let mrsync_block_info =
+    Umarshal.(prod5 int int int int32bigarray Bytearray.m
+                (fun {blockSize; blockCount; checksumSize; weakChecksum; strongChecksum} ->
+                  blockSize, blockCount, checksumSize, weakChecksum, strongChecksum)
+                (fun (blockSize, blockCount, checksumSize, weakChecksum, strongChecksum) ->
+                  {blockSize; blockCount; checksumSize; weakChecksum; strongChecksum}))
+
   (*** PREPROCESS ***)
 
   (* Worst case probability of a failure *)
diff --git a/src/transfer.mli b/src/transfer.mli
index 0feb7b4..1212882 100644
--- a/src/transfer.mli
+++ b/src/transfer.mli
@@ -77,6 +77,8 @@ module Rsync :
     (* Built from the old file by the destination computer *)
     type rsync_block_info
 
+    val mrsync_block_info : rsync_block_info Umarshal.t
+
     (* Expected size of the [rsync_block_info] datastructure (in KiB). *)
     val memoryFootprint : Uutil.Filesize.t -> Uutil.Filesize.t -> int
 
diff --git a/src/transport.ml b/src/transport.ml
index de827da..533c664 100644
--- a/src/transport.ml
+++ b/src/transport.ml
@@ -37,17 +37,50 @@ let fileSize uiFrom uiTo =
 
 let maxthreads =
   Prefs.createInt "maxthreads" 0
-    "!maximum number of simultaneous file transfers"
+    ~category:(`Advanced `General)
+    "maximum number of simultaneous file transfers"
     ("This preference controls how much concurrency is allowed during \
       the transport phase.  Normally, it should be set reasonably high \
       to maximize performance, but when Unison is used over a \
       low-bandwidth link it may be helpful to set it lower (e.g. \
       to 1) so that Unison doesn't soak up all the available bandwidth. \
       The default is the special value 0, which mean 20 threads \
-      when file content streaming is desactivated and 1000 threads \
+      when file content streaming is deactivated and 1000 threads \
       when it is activated.")
 
-let actionReg = Lwt_util.make_region 50
+let maxThreads () =
+  let n = Prefs.read maxthreads in
+  if n > 0 then n else
+  if Prefs.read Remote.streamingActivated then 1000 else 20
+
+let run dispenseTask =
+  let runConcurrent limit dispenseTask =
+    let dispenseTask () = if Abort.isAll () then None else dispenseTask () in
+    let avail = ref limit in
+    let rec runTask thr =
+      Lwt.try_bind thr
+        (fun () -> nextTask (); Lwt.return ())
+        (fun _ -> nextTask (); assert false)
+        (* It is a programming error for an exception to reach this far. *)
+      |> ignore
+    and nextTask () =
+       match dispenseTask () with
+       | None -> incr avail
+       | Some thr -> runTask thr
+    in
+    let rec fillPool () =
+      match dispenseTask () with
+      | None -> ()
+      | Some thr -> decr avail; runTask thr; if !avail > 0 then fillPool ()
+    in
+    fillPool ()
+  in
+  (* When streaming, we can transfer many file simultaneously:
+     as the contents of only one file is transferred in one direction
+     at any time, little resource is consumed this way. *)
+  let limit = maxThreads () in
+  Lwt_util.resize_region !Files.copyReg limit;
+  runConcurrent limit dispenseTask
 
 (* Logging for a thread: write a message before and a message after the
    execution of the thread. *)
@@ -79,66 +112,55 @@ let logLwtNumbered (lwtDescription: string) (lwtShortDescription: string)
 
 let doAction
       fromRoot fromPath fromContents toRoot toPath toContents notDefault id =
-  (* When streaming, we can transfer many file simultaneously:
-     as the contents of only one file is transferred in one direction
-     at any time, little resource is consumed this way. *)
-  let limit =
-    let n = Prefs.read maxthreads in
-    if n > 0 then n else
-    if Prefs.read Remote.streamingActivated then 1000 else 20
-  in
-  Lwt_util.resize_region actionReg limit;
-  Lwt_util.resize_region Files.copyReg limit;
-  Lwt_util.run_in_region actionReg 1 (fun () ->
-    if not !Trace.sendLogMsgsToStderr then
-      Trace.statusDetail (Path.toString toPath);
-    Remote.Thread.unwindProtect (fun () ->
-      match fromContents, toContents with
-          {typ = `ABSENT}, {ui = uiTo} ->
-             logLwtNumbered
-               ("Deleting " ^ Path.toString toPath ^
-                "\n  from "^ root2string toRoot)
-               ("Deleting " ^ Path.toString toPath)
-               (fun () ->
-                  Files.delete fromRoot fromPath toRoot toPath uiTo notDefault)
-        (* No need to transfer the whole directory/file if there were only
-           property modifications on one side.  (And actually, it would be
-           incorrect to transfer a directory in this case.) *)
-        | {status= `Unchanged | `PropsChanged; desc= fromProps; ui= uiFrom},
-          {status= `Unchanged | `PropsChanged; desc= toProps; ui = uiTo} ->
-            logLwtNumbered
-              ("Copying properties for " ^ Path.toString toPath
-               ^ "\n  from " ^ root2string fromRoot ^ "\n  to " ^
-               root2string toRoot)
-              ("Copying properties for " ^ Path.toString toPath)
-              (fun () ->
-                Files.setProp
-                  fromRoot fromPath toRoot toPath fromProps toProps uiFrom uiTo)
-        | {typ = `FILE; ui = uiFrom}, {typ = `FILE; ui = uiTo} ->
-            logLwtNumbered
-              ("Updating file " ^ Path.toString toPath ^ "\n  from " ^
-               root2string fromRoot ^ "\n  to " ^
-               root2string toRoot)
-              ("Updating file " ^ Path.toString toPath)
-              (fun () ->
-                Files.copy (`Update (fileSize uiFrom uiTo))
-                  fromRoot fromPath uiFrom [] toRoot toPath uiTo []
-                  notDefault id)
-        | {ui = uiFrom; props = propsFrom}, {ui = uiTo; props = propsTo} ->
-            logLwtNumbered
-              ("Copying " ^ Path.toString toPath ^ "\n  from " ^
-               root2string fromRoot ^ "\n  to " ^
-               root2string toRoot)
-              ("Copying " ^ Path.toString toPath)
-              (fun () ->
-                 Files.copy `Copy
-                   fromRoot fromPath uiFrom propsFrom
-                   toRoot toPath uiTo propsTo
-                   notDefault id))
-      (fun e -> Trace.log
-          (Printf.sprintf
-             "Failed: %s\n" (Util.printException e));
-        return ()))
+  if not !Trace.sendLogMsgsToStderr then
+    Trace.statusDetail (Path.toString toPath);
+  Remote.Thread.unwindProtect (fun () ->
+    match fromContents, toContents with
+        {typ = `ABSENT}, {ui = uiTo} ->
+           logLwtNumbered
+             ("Deleting " ^ Path.toString toPath ^
+              "\n  from "^ root2string toRoot)
+             ("Deleting " ^ Path.toString toPath)
+             (fun () ->
+                Files.delete fromRoot fromPath toRoot toPath uiTo notDefault)
+      (* No need to transfer the whole directory/file if there were only
+         property modifications on one side.  (And actually, it would be
+         incorrect to transfer a directory in this case.) *)
+      | {status= `Unchanged | `PropsChanged; desc= fromProps; ui= uiFrom},
+        {status= `Unchanged | `PropsChanged; desc= toProps; ui = uiTo} ->
+          logLwtNumbered
+            ("Copying properties for " ^ Path.toString toPath
+             ^ "\n  from " ^ root2string fromRoot ^ "\n  to " ^
+             root2string toRoot)
+            ("Copying properties for " ^ Path.toString toPath)
+            (fun () ->
+              Files.setProp
+                fromRoot fromPath toRoot toPath fromProps toProps uiFrom uiTo)
+      | {typ = `FILE; ui = uiFrom}, {typ = `FILE; ui = uiTo} ->
+          logLwtNumbered
+            ("Updating file " ^ Path.toString toPath ^ "\n  from " ^
+             root2string fromRoot ^ "\n  to " ^
+             root2string toRoot)
+            ("Updating file " ^ Path.toString toPath)
+            (fun () ->
+              Files.copy (`Update (fileSize uiFrom uiTo))
+                fromRoot fromPath uiFrom [] toRoot toPath uiTo []
+                notDefault id)
+      | {ui = uiFrom; props = propsFrom}, {ui = uiTo; props = propsTo} ->
+          logLwtNumbered
+            ("Copying " ^ Path.toString toPath ^ "\n  from " ^
+             root2string fromRoot ^ "\n  to " ^
+             root2string toRoot)
+            ("Copying " ^ Path.toString toPath)
+            (fun () ->
+               Files.copy `Copy
+                 fromRoot fromPath uiFrom propsFrom
+                 toRoot toPath uiTo propsTo
+                 notDefault id))
+    (fun e -> Trace.logonly
+        (Printf.sprintf
+           "Failed [%s]: %s\n" (Path.toString toPath) (Util.printException e));
+      return ())
 
 let propagate root1 root2 reconItem id showMergeFn =
   let path = reconItem.path1 in
diff --git a/src/transport.mli b/src/transport.mli
index 6f700f9..3707844 100644
--- a/src/transport.mli
+++ b/src/transport.mli
@@ -1,6 +1,17 @@
 (* Unison file synchronizer: src/transport.mli *)
 (* Copyright 1999-2020, Benjamin C. Pierce (see COPYING for details) *)
 
+(* Size of the pool of threads for executing transport actions. *)
+val maxThreads : unit -> int
+
+(* Run tasks concurrently in a pool of threads, acquiring tasks with
+   the supplied task dispenser function. The tasks received from
+   the task dispenser must not raise uncaught exceptions or return
+   with [Lwt.fail]. *)
+val run :
+     (unit -> (unit -> unit Lwt.t) option) (* Task dispenser *)
+  -> unit
+
 (* Executes the actions implied by the reconItem list. *)
 val transportItem :
      Common.reconItem                 (* Updates that need to be performed *)
diff --git a/src/tree.ml b/src/tree.ml
index d09232b..8f4248f 100644
--- a/src/tree.ml
+++ b/src/tree.ml
@@ -20,6 +20,19 @@ type ('a, 'b) t =
     Node of ('a * ('a, 'b) t) list * 'b option
   | Leaf of 'b
 
+let m_rec ma mb m =
+  Umarshal.(sum2
+              (prod2 (list (prod2 ma m id id)) (option mb) id id)
+              mb
+              (function
+               | Node (a, b) -> I21 (a, b)
+               | Leaf a -> I22 a)
+              (function
+               | I21 (a, b) -> Node (a, b)
+               | I22 a -> Leaf a))
+
+let m ma mb = Umarshal.rec1 (m_rec ma mb)
+
 type ('a, 'b) u =
   { anc: (('a, 'b) u * 'a) option;
     node: 'b option;
diff --git a/src/tree.mli b/src/tree.mli
index 13d4041..a1688b9 100644
--- a/src/tree.mli
+++ b/src/tree.mli
@@ -7,6 +7,8 @@ type ('a, 'b) t =
     Node of ('a * ('a, 'b) t) list * 'b option
   | Leaf of 'b
 
+val m : 'a Umarshal.t -> 'b Umarshal.t -> ('a, 'b) t Umarshal.t
+
 (* An "unfinished" tree                                                      *)
 type ('a, 'b) u
 
diff --git a/src/ubase/META b/src/ubase/META
deleted file mode 100644
index 1ce33a7..0000000
--- a/src/ubase/META
+++ /dev/null
@@ -1,4 +0,0 @@
-requires = "unix"
-version = "0.1"
-archive(byte) = "ubase.cma"
-archive(native) = "ubase.cmxa"
diff --git a/src/ubase/Makefile b/src/ubase/Makefile
deleted file mode 100644
index c37a5ef..0000000
--- a/src/ubase/Makefile
+++ /dev/null
@@ -1,57 +0,0 @@
-NAME = ubase
-
-OBJECTS  = \
-  safelist.cmo uprintf.cmo util.cmo uarg.cmo prefs.cmo trace.cmo rx.cmo \
-  myMap.cmo
-
-OCAMLC = ocamlfind ocamlc -g 
-OCAMLOPT = ocamlfind ocamlopt 
-OCAMLDEP = ocamldep
-
-XOBJECTS = $(OBJECTS:cmo=cmx)
-
-ARCHIVE  = $(NAME).cma
-XARCHIVE = $(NAME).cmxa
-
-REQUIRES =
-PREDICATES =
-
-all: $(ARCHIVE) 
-opt: $(XARCHIVE)
-
-$(ARCHIVE): $(OBJECTS)
-	$(OCAMLC) -a -o $(ARCHIVE) -package "$(REQUIRES)" -linkpkg \
-	          -predicates "$(PREDICATES)" $(OBJECTS)
-$(XARCHIVE): $(XOBJECTS)
-	$(OCAMLOPT) -a -o $(XARCHIVE) -package "$(REQUIRES)" -linkpkg \
-	          -predicates "$(PREDICATES)" $(XOBJECTS)
-
-.SUFFIXES: .cmo .cmi .cmx .ml .mli
-
-.ml.cmo:
-	$(OCAMLC) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-.mli.cmi:
-	$(OCAMLC) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-.ml.cmx:
-	$(OCAMLOPT) -package "$(REQUIRES)" -predicates "$(PREDICATES)" \
-	          -c $<
-
-depend: *.ml *.mli
-	$(OCAMLDEP) *.ml *.mli > depend
-include depend
-
-install: all
-	{ test ! -f $(XARCHIVE) || extra="$(XARCHIVE) "`basename $(XARCHIVE) .cmxa`.a; }; \
-	ocamlfind install $(NAME) *.mli *.cmi $(ARCHIVE) META $$extra
-
-uninstall:
-	ocamlfind remove $(NAME)
-
-clean::
-	rm -f *.cmi *.cmo *.cmx *.cma *.cmxa *.a *.o *~ *.bak
-
-# Used by BCP to update Harmony's copy of these files from Unison's
-update:
-	cp $(HOME)/current/unison/trunk/src/ubase/{*.ml,*.mli,Makefile} .
\ No newline at end of file
diff --git a/src/ubase/myMap.ml b/src/ubase/myMap.ml
index 8b21ac0..213c600 100644
--- a/src/ubase/myMap.ml
+++ b/src/ubase/myMap.ml
@@ -18,6 +18,7 @@ Some functions have been added to suite Unison needs.
 module type OrderedType =
   sig
     type t
+    val m : t Umarshal.t
     val compare: t -> t -> int
   end
 
@@ -25,6 +26,7 @@ module type S =
   sig
     type key
     type +'a t
+    val m : 'a Umarshal.t -> 'a t Umarshal.t
     val empty: 'a t
     val is_empty: 'a t -> bool
     val add: key -> 'a -> 'a t -> 'a t
@@ -262,4 +264,8 @@ module Make(Ord: OrderedType) = struct
           `Ok
       | Node (l, v, _, r, _) ->
           val_combine (validate_left l v) (validate_right v r)
+
+    let m m = Umarshal.(sum1 (list (prod2 Ord.m m id id))
+                          (fun x -> fold (fun k v accu -> (k, v) :: accu) x [])
+                          (fun x -> List.fold_left (fun accu (k, v) -> add k v accu) empty x))
 end
diff --git a/src/ubase/myMap.mli b/src/ubase/myMap.mli
index 3fcd0f5..9b3f0f1 100644
--- a/src/ubase/myMap.mli
+++ b/src/ubase/myMap.mli
@@ -29,6 +29,7 @@ module type OrderedType =
   sig
     type t
       (** The type of the map keys. *)
+    val m : t Umarshal.t
     val compare : t -> t -> int
       (** A total ordering function over the keys.
           This is a two-argument function [f] such that
@@ -48,6 +49,8 @@ module type S =
     type (+'a) t
     (** The type of maps from type [key] to type ['a]. *)
 
+    val m : 'a Umarshal.t -> 'a t Umarshal.t
+
     val empty: 'a t
     (** The empty map. *)
 
diff --git a/src/ubase/prefs.ml b/src/ubase/prefs.ml
index 7b96384..bb840e1 100644
--- a/src/ubase/prefs.ml
+++ b/src/ubase/prefs.ml
@@ -80,27 +80,35 @@ let resetToDefaults () =
 
 type dumpedPrefs = (string * bool * string) list
 
-let dumpers = ref ([] : (string * bool * (unit->string)) list)
-let loaders = ref (Util.StringMap.empty : (string->unit) Util.StringMap.t)
+let mdumpedPrefs = Umarshal.(list (prod3 string bool string id id))
 
-let adddumper name optional f =
-  dumpers := (name,optional,f) :: !dumpers
+let dumpers = ref ([] : (string * bool * (unit->bool) * (int->string)) list)
+let loaders = ref (Util.StringMap.empty : (int->string->unit) Util.StringMap.t)
+let ignored = ref []
+
+let adddumper name optional send f =
+  dumpers := (name,optional,send,f) :: !dumpers
 
 let addloader name f =
   loaders := Util.StringMap.add name f !loaders
 
-let dump () = Safelist.map (fun (name, opt, f) -> (name, opt, f())) !dumpers
+let addignored name =
+  ignored := name :: !ignored
+
+let dump rpcVer =
+  Safelist.filter (fun (_, _, sf, _) -> sf ()) !dumpers
+  |> Safelist.map (fun (name, opt, _, f) -> (name, opt, f rpcVer))
 
-let load d =
+let load d rpcVer =
   Safelist.iter
     (fun (name, opt, dumpedval) ->
        match
          try Some (Util.StringMap.find name !loaders) with Not_found -> None
        with
          Some loaderfn ->
-           loaderfn dumpedval
+           loaderfn rpcVer dumpedval
        | None ->
-           if not opt then
+           if not opt && not (Safelist.mem name !ignored) then
              raise (Util.Fatal
                       ("Preference "^name^" not found: \
                         inconsistent Unison versions??")))
@@ -131,101 +139,192 @@ let aliasMap = ref (Util.StringMap.empty : string Util.StringMap.t)
 let canonicalName nm =
   try Util.StringMap.find nm !aliasMap with Not_found -> nm
 
+type topic = [
+  | `General
+  | `Sync
+  | `Syncprocess
+  | `Syncprocess_CLI
+  | `CLI
+  | `GUI
+  | `Remote
+  | `Archive ]
+
+type group = [
+  | `Basic of topic
+  | `Advanced of topic
+  | `Expert
+  | `Internal of
+      [ `Pseudo | `Devel | `Other ] ]
+
+let isInternal = function
+  | `Internal _ -> true
+  | _ -> false
+
+let topic = function
+  | `General -> "General"
+  | `Sync -> "What to sync"
+  | `Syncprocess -> "How to sync"
+  | `Syncprocess_CLI -> "How to sync (text interface (CLI) only)"
+  | `CLI -> "Text interface (CLI)"
+  | `GUI -> "Graphical interface (GUI)"
+  | `Remote -> "Remote connections"
+  | `Archive -> "Archive management"
+
 type typ =
   [`BOOL | `INT | `STRING | `STRING_LIST | `BOOLDEF | `CUSTOM | `UNKNOWN]
 
-(* prefType : prefName -> type *)
-let prefType = ref (Util.StringMap.empty : typ Util.StringMap.t)
-
-let typ nm = try Util.StringMap.find nm !prefType with Not_found -> `UNKNOWN
-
-(* prefs: prefName -> (doc, pspec, fulldoc)                                  *)
+type apref =
+  {
+    category : group;
+    doc : string;
+    pspec : Uarg.spec;
+    fulldoc : string;
+    typ : typ;
+    cli_only : bool;
+    deprec : bool;
+  }
+
+(* prefs: prefName -> apref                                                  *)
 let prefs =
-  ref (Util.StringMap.empty : (string * Uarg.spec * string) Util.StringMap.t)
+  ref (Util.StringMap.empty : apref Util.StringMap.t)
+
+let typ nm =
+  try let {typ; _} = Util.StringMap.find nm !prefs in typ with
+  | Not_found -> `UNKNOWN
 
 let documentation nm =
   try
-    let (doc, _, fulldoc) = Util.StringMap.find nm !prefs in
-    if doc <> "" && doc.[0] = '*' then raise Not_found;
-    let basic = doc = "" || doc.[0] <> '!' in
+    let {category; doc; fulldoc; deprec; _} = Util.StringMap.find nm !prefs in
+    if isInternal category then raise Not_found;
     let doc =
-      if not basic then
-        String.sub doc 1 (String.length doc - 1)
-      else
-        doc
+      if not deprec then doc
+      else "(Deprecated) " ^ doc
     in
-    (doc, fulldoc, basic)
+    let fulldoc =
+      if not deprec then fulldoc
+      else "{\\em (Deprecated)} " ^ fulldoc
+    in
+    (doc, fulldoc)
   with Not_found ->
-    ("", "", false)
+    ("", "")
 
-let list () =
+let category nm =
+  try
+    let {category; _} = Util.StringMap.find nm !prefs in
+    Some category
+  with Not_found ->
+    None
+
+let list include_cli_only =
   List.sort String.compare
-    (Util.StringMap.fold (fun nm _ l -> nm :: l) !prefType [])
+    (Util.StringMap.fold
+      (fun nm {category; cli_only; _} l ->
+        if (not cli_only || include_cli_only) && not (isInternal category) then
+          nm :: l
+        else l)
+      !prefs [])
 
 (* aliased pref has *-prefixed doc and empty fulldoc                         *)
 let alias pref newname =
   (* pref must have been registered, so name pref is not empty, and will be *)
   (* found in the map, no need for catching exception                       *)
-  let (_,pspec,_) = Util.StringMap.find (Safelist.hd (name pref)) !prefs in
-  prefs := Util.StringMap.add newname ("*", pspec, "") !prefs;
+  let pref' = Util.StringMap.find (Safelist.hd (name pref)) !prefs in
+  let pref' = {pref' with category = `Internal `Other; doc = "*"; fulldoc = ""} in
+  prefs := Util.StringMap.add newname pref' !prefs;
+  let () =
+    try
+      let loader = Util.StringMap.find (Safelist.hd (name pref)) !loaders in
+      addloader newname loader
+    with Not_found -> ()
+  in
   aliasMap := Util.StringMap.add newname (Safelist.hd (name pref)) !aliasMap;
   pref.names <- newname :: pref.names
 
-let registerPref name typ pspec doc fulldoc =
+let combine_pspec f = function
+  | Uarg.Bool f' -> Uarg.Bool (fun x -> f' x; f ())
+  | Uarg.String f' -> Uarg.String (fun x -> f' x; f ())
+  | Uarg.Int f' -> Uarg.Int (fun x -> f' x; f ())
+  | _ -> assert false
+
+let deprecatedPref name p =
+  combine_pspec @@ fun () ->
+  Util.warn ("Preference \"" ^ name ^ "\" is deprecated!\n"
+    ^ "It may be removed in the next release, so you should\n"
+    ^ "stop using this preference on the command line and\n"
+    ^ "in the profiles."
+    ^ (if read p <> readDefault p then "" else
+         "\nYou will not lose out on anything; you have currently\n"
+       ^ "set this preference to its default value."))
+
+let registerPref name typ cell pspec category cli_only deprec doc fulldoc =
   if Util.StringMap.mem name !prefs then
     raise (Util.Fatal ("Preference " ^ name ^ " registered twice"));
-  prefs := Util.StringMap.add name (doc, pspec, fulldoc) !prefs;
-  (* Ignore internal preferences *)
-  if doc = "" || doc.[0] <> '*' then
-    prefType := Util.StringMap.add name typ !prefType
-
-let createPrefInternal name typ local default doc fulldoc printer parsefn =
+  let pspec =
+    if not deprec then pspec
+    else deprecatedPref name cell pspec in
+  let pref = {category; doc; pspec; fulldoc; typ; cli_only; deprec} in
+  prefs := Util.StringMap.add name pref !prefs
+
+let createPrefInternal name typ category cli_only local send default deprecated doc fulldoc printer parsefn m =
+  let m = Umarshal.(prod2 m (list string) id id) in
   let newCell = rawPref default name in
-  registerPref name typ (parsefn newCell) doc fulldoc;
+  registerPref name typ newCell (parsefn newCell) category cli_only deprecated doc fulldoc;
+  let (local, send) =
+    if not cli_only then (local, send)
+    else (true, Some (fun () -> false))
+  in
   adddumper name local
-    (fun () -> Marshal.to_string (newCell.value, newCell.names) []);
+    (fun () -> match send with None -> true | Some f -> f ())
+    (function
+     | 0 -> Marshal.to_string (newCell.value, newCell.names) []
+     | _ -> Umarshal.to_string m (newCell.value, newCell.names));
   addprinter name (fun () -> printer newCell.value);
   addresetter
     (fun () ->
        newCell.setInProfile <- false; newCell.value <- newCell.defaultValue);
   addloader name
-    (fun s ->
-       let (value, names) = Marshal.from_string s 0 in
+    (fun rpcVer s ->
+       if not cli_only then   (* Better for compatibility to not fail if cli_only *)
+       let (value, names) =
+         match rpcVer with
+         | 0 -> Marshal.from_string s 0
+         | _ -> Umarshal.from_string m s 0
+       in
        newCell.value <- value);
   newCell
 
-let create name ?(local=false) default doc fulldoc intern printer =
-  createPrefInternal name `CUSTOM local default doc fulldoc printer
+let create name ~category ?(cli_only=false) ?(local=false) ?send default ?(deprecated=false) doc fulldoc intern printer m =
+  createPrefInternal name `CUSTOM category cli_only local send default deprecated doc fulldoc printer
     (fun cell -> Uarg.String (fun s -> set cell (intern (read cell) s)))
+    m
 
-let createBool name ?(local=false) default doc fulldoc =
+let createBool name ~category ?(cli_only=false) ?(local=false) ?send default ?(deprecated=false) doc fulldoc =
   let doc = if default then doc ^ " (default true)" else doc in
-  createPrefInternal name `BOOL local default doc fulldoc
+  createPrefInternal name `BOOL category cli_only local send default deprecated doc fulldoc
     (fun v -> [if v then "true" else "false"])
     (fun cell -> Uarg.Bool (fun b -> set cell b))
+    Umarshal.bool
 
-let createInt name ?(local=false) default doc fulldoc =
-  createPrefInternal name `INT local default doc fulldoc
+let createInt name ~category ?(cli_only=false) ?(local=false) ?send default ?(deprecated=false) doc fulldoc =
+  createPrefInternal name `INT category cli_only local send default deprecated doc fulldoc
     (fun v -> [string_of_int v])
     (fun cell -> Uarg.Int (fun i -> set cell i))
+    Umarshal.int
 
-let createString name ?(local=false) default doc fulldoc =
-  createPrefInternal name `STRING local default doc fulldoc
+let createString name ~category ?(cli_only=false) ?(local=false) ?send default ?(deprecated=false) doc fulldoc =
+  createPrefInternal name `STRING category cli_only local send default deprecated doc fulldoc
     (fun v -> [v])
     (fun cell -> Uarg.String (fun s -> set cell s))
+    Umarshal.string
 
-let createFspath name ?(local=false) default doc fulldoc =
-  createPrefInternal name `STRING local default doc fulldoc
-    (fun v -> [System.fspathToString v])
-    (fun cell -> Uarg.String (fun s -> set cell (System.fspathFromString s)))
-
-let createStringList name ?(local=false) doc fulldoc =
-  createPrefInternal name `STRING_LIST local [] doc fulldoc
+let createStringList name ~category ?(cli_only=false) ?(local=false) ?send ?(deprecated=false) doc fulldoc =
+  createPrefInternal name `STRING_LIST category cli_only local send [] deprecated doc fulldoc
     (fun v -> v)
     (fun cell -> Uarg.String (fun s -> set cell (s:: read cell)))
+    Umarshal.(list string)
 
-let createBoolWithDefault name ?(local=false) doc fulldoc =
-  createPrefInternal name `BOOLDEF local `Default doc fulldoc
+let createBoolWithDefault name ~category ?(cli_only=false) ?(local=false) ?send ?(deprecated=false) doc fulldoc =
+  createPrefInternal name `BOOLDEF category cli_only local send `Default deprecated doc fulldoc
     (fun v -> [match v with
                  `True    -> "true"
                | `False   -> "false"
@@ -240,6 +339,18 @@ let createBoolWithDefault name ?(local=false) doc fulldoc =
               | _                  -> `False
             in
             set cell v))
+    Umarshal.(sum3 unit unit unit
+                (function
+                 | `True -> I31 ()
+                 | `False -> I32 ()
+                 | `Default -> I33 ())
+                (function
+                 | I31 () -> `True
+                 | I32 () -> `False
+                 | I33 () -> `Default))
+
+let markRemoved name =
+  addignored name
 
 (*****************************************************************************)
 (*                     Preferences file parsing                              *)
@@ -261,12 +372,18 @@ let string2int name string =
 (* Takes a filename and returns a list of "parsed lines" containing
       (filename, lineno, varname, value)
    in the same order as in the file. *)
-let rec readAFile ?(fail=true) ?(add_ext=true) filename
-    : (string * int * string * string) list =
+let rec readAFile ?(fail=true) ?(add_ext=true) filename =
+  let path = profilePathname ~add_ext:add_ext filename in
+  let locname =
+    if add_ext then
+      Printf.sprintf "Profile \"%s\" (file \"%s\")" filename path
+    else
+      Printf.sprintf "File \"%s\"" path
+  in
   let bom = "\xef\xbb\xbf" in (* BOM: UTF-8 byte-order mark *)
-  let rec loop chan lines =
+  let rec loop chan lineNum lines =
     match (try Some(input_line chan) with End_of_file -> None) with
-      None -> close_in chan; parseLines filename lines
+      None -> close_in chan; parseLines lines
     | Some(theLine) ->
         let theLine =
           (* A lot of Windows tools start a UTF-8 encoded file by a
@@ -276,40 +393,51 @@ let rec readAFile ?(fail=true) ?(add_ext=true) filename
           else
             theLine
         in
-        loop chan (theLine::lines)
+        loop chan (lineNum + 1) (((locname, lineNum), theLine) :: lines)
   in
   let chan =
     try
-      let path = profilePathname ~add_ext:add_ext filename in
       profileFiles := (path, System.stat path) :: !profileFiles;
       Some (System.open_in_bin path)
     with Unix.Unix_error _ | Sys_error _ -> None
   in
   match chan, fail with
-    None, true ->
-      raise(Util.Fatal(Printf.sprintf "Preference file %s not found" filename))
+  | None, true when add_ext ->
+      raise (Util.Fatal (Printf.sprintf
+        "Profile %s not found (looking for file %s)" filename path))
+  | None, true ->
+      raise (Util.Fatal (Printf.sprintf
+        "Preference file %s not found" path))
   | None, false -> []
-  | Some chan, _ -> loop chan []
+  | Some chan, _ ->
+      try loop chan 1 [] with e -> close_in_noerr chan; raise e
 
 (* Takes a list of strings in reverse order and yields a list of "parsed lines"
    in correct order *)
-and parseLines filename lines =
-  let rec loop lines lineNum res =
+and parseLines lines =
+  let rec loop lines res =
     match lines with
       [] -> res
-    | theLine :: rest ->
+    | (((locname, lineNum) as loc), theLine) :: rest ->
         let theLine = Util.removeTrailingCR theLine in
         let l = Util.trimWhitespace theLine in
         let includes ~fail ~add_ext =
           match Util.splitIntoWords theLine ' ' with
             [_;f] ->
-              let sublines = readAFile f ~fail:fail ~add_ext:add_ext in
-              loop rest (lineNum+1) (Safelist.append sublines res)
+              let sublines =
+                try
+                  readAFile f ~fail:fail ~add_ext:add_ext
+                with Util.Fatal err ->
+                  raise (Util.Fatal (Printf.sprintf
+                    "Included from %s, line %d:\n%s"
+                    (String.uncapitalize_ascii locname) lineNum err))
+              in
+              loop rest (Safelist.append sublines res)
           | _ -> raise (Util.Fatal(Printf.sprintf
-                                     "File \"%s\", line %d:\nGarbled 'include' directive: %s"
-                                     filename lineNum theLine)) in
+                                     "%s, line %d:\nGarbled 'include' directive: %s"
+                                     locname lineNum theLine)) in
         if l = "" || l.[0]='#' then
-          loop rest (lineNum+1) res
+          loop rest res
         else if Util.startswith theLine "include " then
           includes ~fail:true ~add_ext:true
         else if Util.startswith theLine "source " then
@@ -322,19 +450,24 @@ and parseLines filename lines =
           match Util.splitAtChar theLine '=' with
             i, Some j -> let (varName, theResult) = (fun f (i,j) -> (f i,f j))
                   Util.trimWhitespace (i,j) in
-              loop rest (lineNum+1) ((filename, lineNum, varName, theResult)::res)
+              loop rest ((loc, varName, theResult) :: res)
           | _ -> (* theLine does not contain '=' *)
               raise (Util.Fatal(Printf.sprintf
-                                  "File \"%s\", line %d:\nGarbled line (no '='):\n%s"
-                                  filename lineNum theLine)) in
-  loop lines 1 []
+                                  "%s, line %d:\nGarbled line (no '='): %s"
+                                  locname lineNum theLine)) in
+  loop lines []
 
 let processLines lines =
   Safelist.iter
-    (fun (fileName, lineNum, varName,theResult) ->
+    (fun ((locName, lineNum), varName, theResult) ->
        try
-         let _, theFunction, _ = Util.StringMap.find varName !prefs in
-         match theFunction with
+         let pref = Util.StringMap.find varName !prefs in
+         if pref.category = `Internal `Pseudo then raise Not_found;
+         if pref.cli_only then
+           raise (IllegalValue ("\"" ^ varName
+             ^ "\" is a command line-only option; "
+             ^ "it must not be present in a profile."));
+         match pref.pspec with
            Uarg.Bool boolFunction ->
              boolFunction (string2bool varName theResult)
          | Uarg.Int intFunction ->
@@ -343,11 +476,11 @@ let processLines lines =
              stringFunction theResult
          | _ -> assert false
        with Not_found ->
-         raise (Util.Fatal ("File \""^ fileName ^ "\", line " ^
+         raise (Util.Fatal (locName ^ ", line " ^
                             string_of_int lineNum ^ ": `" ^
                             varName ^ "' is not a valid option"))
        | IllegalValue str ->
-           raise(Util.Fatal("File \""^ fileName ^ "\", line " ^
+           raise (Util.Fatal (locName ^ ", line " ^
                             string_of_int lineNum ^ ": " ^ str)))
     lines
 
@@ -357,19 +490,36 @@ let loadTheFile () =
   | Some(n) -> processLines(readAFile n)
 
 let loadStrings l =
-  processLines (parseLines "<internal>" l)
+  let rec loop n out = function
+    | [] -> processLines (parseLines out)
+    | h :: t -> loop (n + 1) ((("<internal preferences>", n), h) :: out) t
+  in
+  loop 1 [] l
 
 (*****************************************************************************)
 (*                      Command-line parsing                                 *)
 (*****************************************************************************)
 
-let opts = ref
-    [("source",
-      Uarg.String (fun s -> processLines @@ readAFile ~add_ext:false s),
-      "include a file's preferences");
-     ("include",
-      Uarg.String (fun s -> processLines @@ readAFile s),
-      "include a profile file's preferences")]
+let _ = create "source" ()
+  ~category:(`Advanced `General)
+  ~cli_only:true
+  "include a file's preferences"
+  "Include preferences from a file.  \\texttt{source \\ARG{name}} reads the \
+   file \\showtt{name} in the \\texttt{.unison} directory and includes its \
+   contents as if it was part of a profile or given directly on command line."
+  (fun _ s -> processLines (readAFile ~add_ext:false s))
+  (fun v -> []) Umarshal.unit
+
+let _ = create "include" ()
+  ~category:(`Advanced `General)
+  ~cli_only:true
+  "include a profile's preferences"
+  "Include preferences from a profile.  \\texttt{include \\ARG{name}} reads \
+   the profile \\showtt{name} (or file \\showtt{name} in the \\texttt{.unison} \
+   directory if profile \\showtt{name} does not exist) and includes its \
+   contents as if it was part of a profile or given directly on command line."
+  (fun _ s -> processLines (readAFile s))
+  (fun v -> []) Umarshal.unit
 
 let prefArg = function
     Uarg.Bool(_)   -> ""
@@ -377,49 +527,83 @@ let prefArg = function
   | Uarg.String(_) -> "xxx"
   | _             -> assert false
 
-(* [argspecs hook] returns a list of specs for [Uarg.parse] *)
+(* Prepare a list of specs for [Uarg.parse] *)
 let argspecs hook =
-  let f (name, pspec, doc) l =
-    ("-" ^ name, hook name pspec, "") :: l in
-  Safelist.fold_right f !opts @@
-  Util.StringMap.fold (fun name (doc, pspec, _) -> f (name, pspec, doc)) !prefs
-    []
-
-let oneLineDocs u =
-  let formatOne name pspec doc p =
-    (* if [p] format a one line message documenting a preference *)
-    if not p then "" else
-    let doc = if doc.[0] = '!'
-                then String.sub doc 1 ((String.length doc) - 1)
-                else doc in
+  Util.StringMap.fold
+    (fun name pref l ->
+       if pref.category <> `Internal `Pseudo then
+         ("-" ^ name, hook name pref.pspec, "") :: l
+       else l)
+    !prefs []
+
+let title = function
+  | `Advanced `Sync -> "Fine-tune sync"
+  | `Advanced `General -> "Other"
+  | `Basic t | `Advanced t -> topic t
+  | `Expert -> ""
+  | `Internal _ -> assert false
+let topic_title = title
+
+let topicsInOrder = [ `Sync; `Syncprocess; `Syncprocess_CLI; `CLI; `GUI; `Remote; `Archive ]
+
+let oneLineDocs ?(hpre="") ?(hpost="") u =
+  let buf = Buffer.create 1024 in
+  let out = Buffer.add_string buf in
+  let fmt = Format.formatter_of_buffer buf in
+  let () = Format.pp_set_margin fmt 81 in  (* cols + 1 *)
+
+  let formatPref name {pspec; doc; deprec; _ } =
     let arg = prefArg pspec in
-    let arg = if arg = "" then "" else " " ^ arg in
-    let spaces =
-      String.make (max 1 (18 - String.length (name ^ arg))) ' ' in
-    " -" ^ name ^ arg ^ spaces ^ doc ^ "\n" in
-  let formatAll p =
-    (* format a message documenting non hidden preferences matching [p] *)
-    String.concat "" @@
-    Safelist.rev @@
-    (fun f i l -> Util.StringMap.fold f l i)
-       (fun name (doc, pspec, _) l ->
-          (formatOne name pspec doc
-             (String.length doc > 0 && doc.[0] <> '*' && p doc)) :: l)
-       [] @@
-    !prefs
+    let s = if arg = "" then name else name ^ " " ^ arg in
+    let l = max 1 (19 - String.length s) in
+    Format.pp_print_string fmt ("   -" ^ s);
+    Format.pp_open_box fmt l;
+    Format.pp_print_break fmt l (1 - l);
+    if deprec then begin
+      Format.pp_print_string fmt "(deprecated)";
+      Format.pp_print_space fmt ()
+    end;
+    Format.pp_print_text fmt doc;
+    Format.pp_close_box fmt ();
+    Format.pp_print_newline fmt ()
+  in
+  let formatTopic t =
+    let m = Util.StringMap.filter (fun _ pref -> pref.category = t) !prefs in
+    if Util.StringMap.cardinal m > 0 then begin
+      let h = title t in
+      if h <> "" then begin
+        out "\n"; out hpre; out "  ";
+        out h;
+        out ":"; out hpost; out "\n"
+      end;
+      Util.StringMap.iter formatPref m
+    end
+  in
+  let formatTopics g =
+    Safelist.iter (fun t -> formatTopic (g t))
   in
-    u ^ "\n"
-  ^ "Basic options: \n"
-  ^ formatAll (fun doc -> doc.[0] <> '!')
-  ^ "\nAdvanced options: \n"
-  ^ formatAll (fun doc -> doc.[0] = '!')
-  ^ Safelist.fold_right
-      (fun (name, pspec, doc) msg -> msg ^ formatOne name pspec doc true)
-      !opts "\nSpecial command line options: \n"
+
+  out u; if u <> "" then out "\n";
+
+  out (hpre ^ "Basic options:" ^ hpost ^ "\n");
+  formatTopics (fun t -> `Basic t) (`General :: topicsInOrder);
+
+  out ("\n" ^ hpre ^ "Advanced options:" ^ hpost ^ "\n");
+  formatTopics (fun t -> `Advanced t) (topicsInOrder @ [`General]);
+
+  out ("\n" ^ hpre ^ "Expert options:" ^ hpost ^ "\n");
+  formatTopic (`Expert);
+
+  Buffer.contents buf
 
 let printUsage usage = Uarg.usage (argspecs (fun _ s -> s))
                          (oneLineDocs usage)
 
+let printUsageForMan () =
+  print_string ".Bd -literal\n";
+  print_string (oneLineDocs ~hpre:".Sy \"" ~hpost:"\"" "");
+  print_string ".Ed\n"
+
 let processCmdLine usage hook =
   Uarg.current := 0;
   let argspecs = argspecs hook in
@@ -430,7 +614,7 @@ let processCmdLine usage hook =
   in
   let anonfun =
     try
-      let (_, p, _) = Util.StringMap.find "rest" !prefs in
+      let {pspec = p; _} = Util.StringMap.find "rest" !prefs in
       match hook "rest" p with
         Uarg.String stringFunction -> stringFunction
       | _                         -> defaultanonfun
@@ -469,28 +653,111 @@ let scanCmdLine usage =
 let listVisiblePrefs () =
   let l =
     Util.StringMap.fold
-      (fun name (_, pspec, fulldoc) l ->
-         if String.length fulldoc > 0 then begin
-           (name, pspec, fulldoc) :: l
+      (fun name ({category; _} as pref) l ->
+         if not (isInternal category) then begin
+           (name, pref) :: l
          end else l) !prefs [] in
-  Safelist.stable_sort (fun (name1,_,_) (name2,_,_) -> compare name1 name2) l
+  Safelist.stable_sort (fun (name1, _) (name2, _) -> compare name1 name2) l
 
-let printFullDocs () =
+let printFullTeXDocs () =
   Printf.eprintf "\\begin{description}\n";
   Safelist.iter
-    (fun (name, pspec, fulldoc) ->
-       Printf.eprintf "\\item [{%s \\tt %s}]\n%s\n\n"
-         name (prefArg pspec) fulldoc)
+    (fun (name, {pspec; fulldoc; deprec; _}) ->
+       Printf.eprintf "\\item [{%s \\tt %s}]\n%s%s\n\n"
+         name (prefArg pspec) (if deprec then "{\\em (Deprecated)} " else "") fulldoc)
     (listVisiblePrefs());
   Printf.eprintf "\\end{description}\n"
 
+let printFullManDocs () =
+  (* The output mangling code is taken from uigtk2.ml with some modifications.
+     Performance is not critical here, it is only run during the build,
+     never by users. *)
+  let (>>>) x f = f x in
+  let emptylineRe = Str.regexp "\n\n+" in
+  let newlineRe = Str.regexp "\n *" in
+  let nodotRe = Str.regexp "^\\([^.\n]+\\)" in
+  let macroRe = Str.regexp "\\(\\.[ \n]*\\)\\([A-Z]\\)" in
+  let styleRe = Str.regexp "\\([^ ]?\\){\\\\\\([a-z]+\\) \\([^{}]*\\)}\\(\\([^ }][^ ]*\\)?\\)" in
+  let verbRe = Str.regexp "\\([^ ]?\\)\\\\verb|\\([^|]*\\)|\\(\\([^ }][^ ]*\\)?\\)" in
+  let argRe = Str.regexp "\\([^ ]?\\)\\\\ARG{\\([^{}]*\\)}\\([^ }]*\\)" in
+  let textttRe = Str.regexp "\\([^ ]?\\)\\\\texttt{\\([^{}]*\\)}\\(\\([^ }][^ ]*\\)?\\)" in
+  let showttRe = Str.regexp "\\([^ ]?\\)\\\\showtt{\\([^{}]*\\)}\\([^ }]*\\)" in
+  let emphRe = Str.regexp "\\([^ ]?\\)\\\\emph{\\([^{}]*\\)}\\([^ }]*\\)" in
+  let sectionRe = Str.regexp "\\\\sectionref{\\([^{}]*\\)}{\\([^{}]*\\)}" in
+  let emdash = Str.regexp_string "---" in
+  let parRe = Str.regexp "\\\\par *" in
+  let underRe = Str.regexp "\\\\_ *" in
+  let dollarRe = Str.regexp "\\\\\\$ *" in
+  let dquotRe = Str.regexp "\"" in
+  let nn1Re = Str.regexp "\\(\\( -NN-\\)+ -NN-\\|\\( -NN-\\)* -NS-\\)\\." in
+  let nn2Re = Str.regexp "\\( -NN-\\)+" in
+  let substMacro m s =
+    (match Str.matched_group 1 s with "" -> " -NN-." | s -> s ^ " -NS-.") ^
+    m ^
+    (Str.matched_group 2 s) ^
+    (match Str.matched_group 3 s with "" -> "" | s -> " Ns " ^ s) ^
+    " -NN-"
+  in
+  let tex2man doc =
+    doc >>>
+    Str.global_replace macroRe "\\1\\&\\2" >>>
+    Str.global_substitute styleRe
+      (fun s ->
+         try
+           let tag =
+             match Str.matched_group 2 s with
+               "em" -> ".Em"
+             | "tt" -> ".Sy"
+             | _ -> raise Exit
+           in
+           Printf.sprintf "%s%s %s%s -NN-"
+             (match Str.matched_group 1 s with "" -> " -NN-" | s -> s ^ " -NS-")
+             tag
+             (Str.matched_group 3 s)
+             (match Str.matched_group 4 s with "" -> "" | s -> " Ns " ^ s)
+         with Exit ->
+           Str.matched_group 0 s) >>>
+    Str.global_substitute verbRe (substMacro "Ic ") >>>
+    Str.global_substitute argRe (substMacro "Ar ") >>>
+    Str.global_substitute textttRe (substMacro "Sy ") >>>
+    Str.global_substitute showttRe (substMacro "Dq ") >>>
+    Str.global_substitute emphRe (substMacro "Em ") >>>
+    Str.global_replace sectionRe "Section\n.Dq \\2\n in the manual" >>>
+    Str.global_replace emdash "\xe2\x80\x94" >>>
+    Str.global_replace parRe "\n" >>>
+    Str.global_replace underRe "_" >>>
+    Str.global_replace dollarRe "$" >>>
+    Str.global_replace dquotRe "\\&\"" >>>
+    Str.global_replace nn1Re " Ns " >>>
+    Str.global_replace nn2Re "\n" >>>
+    Str.global_replace newlineRe "\n" >>>
+    Str.global_replace emptylineRe "\n" >>>
+    Str.global_replace nodotRe ".No \\1" >>>
+    Util.trimWhitespace
+  in
+  Printf.printf ".Bl -tag\n";
+  Safelist.iter
+    (fun (name, {pspec; fulldoc; deprec; _}) ->
+       Printf.printf ".It Ic %s%s\n%s%s\n"
+         name
+         (match prefArg pspec with "" -> "" | s -> " Ar " ^ s)
+         (if deprec then ".Em ( Deprecated )\n" else "")
+         (tex2man fulldoc)
+    )
+    (listVisiblePrefs());
+  Printf.printf ".El\n"
+
+let printFullDocs = function
+  | `TeX -> printFullTeXDocs ()
+  | `man -> printFullManDocs ()
 
 (*****************************************************************************)
 (*                  Adding stuff to the prefs file                           *)
 (*****************************************************************************)
 
 let addprefsto = createString "addprefsto" ""
-  "!file to add new prefs to"
+  ~category:(`Advanced `General)
+  "file to add new prefs to"
   "By default, new preferences added by Unison (e.g., new \\verb|ignore| \
    clauses) will be appended to whatever preference file Unison was told \
    to load at the beginning of the run.  Setting the preference \
@@ -506,7 +773,7 @@ let addLine l =
     debug (fun() ->
       Util.msg "Adding '%s' to %s\n" l (System.fspathToDebugString filename));
     let resultmsg =
-      l ^ "' added to profile " ^ System.fspathToPrintString filename in
+      l ^ "' added to profile " ^ filename in
     let ochan =
       System.open_out_gen [Open_wronly; Open_creat; Open_append] 0o600 filename
     in
diff --git a/src/ubase/prefs.mli b/src/ubase/prefs.mli
index 0aab224..c6759eb 100644
--- a/src/ubase/prefs.mli
+++ b/src/ubase/prefs.mli
@@ -9,52 +9,88 @@ val name : 'a t -> string list
 val overrideDefault : 'a t -> 'a -> unit
 val readDefault : 'a t -> 'a
 
+type topic = [
+  | `General
+  | `Sync
+  | `Syncprocess
+  | `Syncprocess_CLI
+  | `CLI
+  | `GUI
+  | `Remote
+  | `Archive ]
+
+type group = [
+  | `Basic of topic
+  | `Advanced of topic
+  | `Expert
+  | `Internal of         (* Preferences that are not listed *)
+      [ `Pseudo          (* Pseudo-preferences for internal propagation *)
+      | `Devel           (* Developer-only or build-related preferences *)
+      | `Other ]         (* Other non-listed preferences *)
+  ]
+
+(* Note about command line-only preferences. These preferences are never     *)
+(* sent to a server (ignoring [local] and [send] arguments). Should a client *)
+(* send such a preference anyway then the server silently ignores it.        *)
+
 (* Convenient functions for registering simple kinds of preferences.  Note   *)
 (* that createStringPref creates a preference that can only be set once,     *)
 (* while createStringListPref creates a reference to a list of strings that  *)
 (* accumulates a list of values.                                             *)
 val createBool :
         string              (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)  (* whether preference should be sent to server *)
      -> bool                (* initial value *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string              (* documentation string *)
      -> string              (* full (tex) documentation string *)
      -> bool t              (*   -> new preference value *)
 
 val createInt :
         string              (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)  (* whether preference should be sent to server *)
      -> int                 (* initial value *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string              (* documentation string *)
      -> string              (* full (tex) documentation string *)
      -> int t               (*   -> new preference value *)
 
 val createString :
         string              (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)  (* whether preference should be sent to server *)
      -> string              (* initial value *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string              (* documentation string *)
      -> string              (* full (tex) documentation string *)
      -> string t            (*   -> new preference value *)
 
-val createFspath :
-        string              (* preference name *)
-     -> ?local:bool             (* whether it is local to the client *)
-     -> System.fspath       (* initial value *)
-     -> string              (* documentation string *)
-     -> string              (* full (tex) documentation string *)
-     -> System.fspath t     (*   -> new preference value *)
-
 val createStringList :
         string              (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)  (* whether preference should be sent to server *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string              (* documentation string *)
      -> string              (* full (tex) documentation string *)
      -> string list t       (*   -> new preference value *)
 
 val createBoolWithDefault :
         string              (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)  (* whether preference should be sent to server *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string              (* documentation string *)
      -> string              (* full (tex) documentation string *)
      -> [`True|`False|`Default] t
@@ -66,13 +102,18 @@ exception IllegalValue of string
 (* IllegalValue if it is passed a string it cannot deal with.                *)
 val create :
         string                  (* preference name *)
+     -> category:group
+     -> ?cli_only:bool      (* only a command line option, not in a profile *)
      -> ?local:bool             (* whether it is local to the client *)
+     -> ?send:(unit->bool)      (* whether the pref should be sent to server *)
      -> 'a                      (* initial value *)
+     -> ?deprecated:bool    (* preference is deprecated (default false) *)
      -> string                  (* documentation string *)
      -> string                  (* full (tex) documentation string *)
      -> ('a->string->'a)        (* interning function for preference values
                                    (1st arg is old value of preference) *)
      -> ('a -> string list)     (* printing function for preference values *)
+     -> 'a Umarshal.t
      -> 'a t                    (*   -> new preference value *)
 
 (* Create an alternate name for a preference (the new name will not appear   *)
@@ -81,6 +122,12 @@ val alias : 'a t              (* existing preference *)
          -> string            (* new name *)
          -> unit
 
+(* Mark a preference name as intentionally removed. A removed preference     *)
+(* does not exist (can't be specified on command line or in a profile) but   *)
+(* will be silently ignored when sent by a remote host (to not break         *)
+(* compatibility with old clients).                                          *)
+val markRemoved : string -> unit
+
 (* Reset all preferences to their initial values                             *)
 val resetToDefaults : unit -> unit
 
@@ -99,6 +146,7 @@ val parseCmdLine :
 val scanCmdLine : string -> (string list) Util.StringMap.t
 
 val printUsage : string -> unit
+val printUsageForMan : unit -> unit
 
 (* ---------------------------------------------------------------------- *)
 
@@ -106,7 +154,7 @@ val printUsage : string -> unit
 val profileName : string option ref
 
 (* Calculate the full pathname of a preference file                          *)
-val profilePathname : ?add_ext:bool -> string -> System.fspath
+val profilePathname : ?add_ext:bool -> string -> string
 
 (* Check whether the profile file is unchanged                               *)
 val profileUnchanged : unit -> bool
@@ -120,9 +168,9 @@ val add : string -> string -> string
 val addComment : string -> unit
 
 (* Scan a given preferences file and return a list of tuples of the form     *)
-(* (fileName, lineno, name, value), without changing any of the preferences  *)
+(* ((locName, lineno), name, value), without changing any of the preferences *)
 val readAFile : ?fail:bool -> ?add_ext:bool -> string
-     -> (string * int * string * string) list
+     -> ((string * int) * string * string) list
 
 (* Parse the preferences file, raising Fatal if there are any problems       *)
 val loadTheFile : unit -> unit
@@ -134,13 +182,15 @@ val loadStrings : string list -> unit
 
 type dumpedPrefs
 
+val mdumpedPrefs : dumpedPrefs Umarshal.t
+
 (* Dump current values of all preferences into a value that can be
    marshalled and sent over the network or stored in a file for fast
    retrieval *)
-val dump : unit -> dumpedPrefs
+val dump : int -> dumpedPrefs
 
 (* Load new values of all preferences from a string created by dump          *)
-val load : dumpedPrefs -> unit
+val load : dumpedPrefs -> int -> unit
 
 (* ------------------------------------------------------------------------- *)
 
@@ -149,10 +199,12 @@ type typ =
 
 val canonicalName : string -> string
 val typ : string -> typ
-val documentation : string -> string * string * bool
-val list : unit -> string list
+val documentation : string -> string * string
+val category : string -> group option
+val list : bool -> string list
+val topic_title : group -> string
 
 (* ------------------------------------------------------------------------- *)
 
-val printFullDocs : unit -> unit
+val printFullDocs : [`TeX | `man] -> unit
 val dumpPrefsToStderr : unit -> unit
diff --git a/src/ubase/proplist.ml b/src/ubase/proplist.ml
index 8c0d6c4..ed3777b 100644
--- a/src/ubase/proplist.ml
+++ b/src/ubase/proplist.ml
@@ -18,13 +18,13 @@
 type 'a key = string
 type t = Obj.t Util.StringMap.t
 
-let names = ref Util.StringSet.empty
+let names = ref Util.StringMap.empty
 
-let register nm =
-  if (Util.StringSet.mem nm !names) then
+let register nm m =
+  if (Util.StringMap.mem nm !names) then
     raise (Util.Fatal
             (Format.sprintf "Property lists: %s already registered!" nm));
-  names := Util.StringSet.add nm !names;
+  names := Util.StringMap.add nm (Obj.repr m) !names;
   nm
 
 let empty = Util.StringMap.empty
@@ -34,3 +34,20 @@ let mem = Util.StringMap.mem
 let find (k : 'a key) m : 'a = Obj.obj (Util.StringMap.find k m)
 
 let add (k : 'a key) (v : 'a) m = Util.StringMap.add k (Obj.repr v) m
+
+let find_m (k : 'a key) : 'a Umarshal.t =
+  try Obj.obj (Util.StringMap.find k !names) with
+  | Not_found -> raise (Util.Fatal (Format.sprintf "Property lists: %s not yet registered!" k))
+
+module S = struct
+  type key = string
+  type value = Obj.t
+  type map = t
+  let cardinal = Util.StringMap.cardinal
+  let empty = Util.StringMap.empty
+  let add = Util.StringMap.add
+  let iter = Util.StringMap.iter
+  let find_m = find_m
+end
+
+include Umarshal.Proplist (S)
diff --git a/src/ubase/proplist.mli b/src/ubase/proplist.mli
index 6ee1bb4..9186dde 100644
--- a/src/ubase/proplist.mli
+++ b/src/ubase/proplist.mli
@@ -4,7 +4,9 @@
 type 'a key
 type t
 
-val register : string -> 'a key
+val m : t Umarshal.t
+
+val register : string -> 'a Umarshal.t -> 'a key
 
 val empty : t
 val mem : 'a key -> t -> bool
diff --git a/src/ubase/trace.ml b/src/ubase/trace.ml
index 9b00d49..32511f0 100644
--- a/src/ubase/trace.ml
+++ b/src/ubase/trace.ml
@@ -30,7 +30,8 @@ let redirect x = (traceprinter := x)
 
 let debugmods =
   Prefs.createStringList "debug"
-    "!debug module xxx ('all' -> everything, 'verbose' -> more)"
+    ~category:`Expert
+    "debug module xxx ('all' -> everything, 'verbose' -> more)"
     ("This preference is used to make Unison print various sorts of "
      ^ "information about what it is doing internally on the standard "
      ^ "error stream.  It can be used many times, each time with the name "
@@ -46,8 +47,9 @@ let debugmods =
      ^ "what bytes are being sent across the network).")
 
 let debugtimes =
-  Prefs.createBool "debugtimes"
-    false "*annotate debugging messages with timestamps" ""
+  Prefs.createBool "debugtimes" false
+    ~category:(`Internal `Devel)
+    "*annotate debugging messages with timestamps" ""
 
 let runningasserver = ref false
 
@@ -107,14 +109,16 @@ let _ = Util.debugPrinter := Some(debug)
 
 let logging =
   Prefs.createBool "log" true
-    "!record actions in logfile"
+    ~category:(`Advanced `General)
+    "record actions in logfile"
     "When this flag is set, Unison will log all changes to the filesystems
      on a file."
 
 let logfile =
-  Prefs.createFspath "logfile"
-    (System.fspathFromString "unison.log")
-    "!logfile name"
+  Prefs.createString "logfile"
+    "unison.log"
+    ~category:(`Advanced `General)
+    "logfile name"
     "By default, logging messages will be appended to the file
      \\verb|unison.log| in your .unison directory.  Set this preference if
      you prefer another file.  It can be a path relative to your .unison directory.
@@ -142,7 +146,7 @@ let rec getLogch() =
   Util.convertUnixErrorsToFatal "getLogch" (fun() ->
   match !logch with
     None ->
-      let prefstr = System.fspathToString (Prefs.read logfile) in
+      let prefstr = Prefs.read logfile in
       let file = Util.fileMaybeRelToUnisonDir prefstr in
       let ch =
         System.open_out_gen [Open_wronly; Open_creat; Open_append] 0o600 file in
@@ -185,13 +189,31 @@ let writeLog s stripColor =
 (* Formatting and displaying messages *)
 
 let terse =
-  Prefs.createBool "terse" false "suppress status messages"
+  Prefs.createBool "terse" false
+    ~category:(`Basic `Syncprocess_CLI)
+    "suppress status messages"
     ("When this preference is set to {\\tt true}, the user "
      ^ "interface will not print status messages.")
 
 type msgtype = Msg | StatusMajor | StatusMinor | Log | LogColor
 type msg = msgtype * string
 
+let mmsgtype = Umarshal.(sum5 unit unit unit unit unit
+                           (function
+                            | Msg -> I51 ()
+                            | StatusMajor -> I52 ()
+                            | StatusMinor -> I53 ()
+                            | Log -> I54 ()
+                            | LogColor -> I55 ())
+                           (function
+                            | I51 () -> Msg
+                            | I52 () -> StatusMajor
+                            | I53 () -> StatusMinor
+                            | I54 () -> Log
+                            | I55 () -> LogColor))
+
+let mmsg = Umarshal.(prod2 mmsgtype string id id)
+
 let defaultMessageDisplayer s =
   if not (Prefs.read terse) then begin
     let show() = if s<>"" then Util.msg "%s\n" s in
@@ -261,6 +283,7 @@ let logverbose s =
 
 let printTimers =
   Prefs.createBool "timers" false
+    ~category:(`Internal `Devel)
     "*print timing information" ""
 
 type timer = string * float
diff --git a/src/ubase/trace.mli b/src/ubase/trace.mli
index bb30a01..ce20cd7 100644
--- a/src/ubase/trace.mli
+++ b/src/ubase/trace.mli
@@ -48,6 +48,8 @@ val statusFormatter : (string -> string -> string) ref
    types of the following) *)
 type msg
 
+val mmsg : msg Umarshal.t
+
 (* The internal routine used for formatting a message to be displayed
    locally.  It calls !messageDisplayer to do the actual work. *)
 val displayMessageLocally : msg -> unit
diff --git a/src/ubase/uarg.ml b/src/ubase/uarg.ml
index 4a10795..03ee688 100644
--- a/src/ubase/uarg.ml
+++ b/src/ubase/uarg.ml
@@ -43,6 +43,13 @@ let usage speclist errmsg =
 
 let current = ref 0;;
 
+let eprintf fmt =
+  Printf.ksprintf (fun s ->
+    if System.has_stderr ~info:s then Printf.eprintf "%s" s else exit 2) fmt
+
+let verify_stdout () =
+  if not (System.has_stdout ~info:"") then exit 37
+
 let parse speclist anonfun errmsg =
   let argv = System.argv () in
   let initpos = !current in
@@ -50,7 +57,7 @@ let parse speclist anonfun errmsg =
     let progname =
       if initpos < Array.length argv then argv.(initpos) else "(?)" in
     begin match error with
-      | Unknown s when s = "-help" -> ()
+      | Unknown s when s = "-help" -> verify_stdout ()
       | Unknown s ->
           eprintf "%s: unknown option `%s'.\n" progname s
       | Missing s ->
diff --git a/src/ubase/umarshal.ml b/src/ubase/umarshal.ml
new file mode 100644
index 0000000..5c5f061
--- /dev/null
+++ b/src/ubase/umarshal.ml
@@ -0,0 +1,641 @@
+(* Unison file synchronizer: src/ubase/umarshal.ml *)
+(* Copyright 2020, Stéphane Glondu
+
+    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.
+
+    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 General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*)
+
+exception Error of string
+
+type 'a t = {
+    read : (bytes -> int -> int -> unit) -> 'a;
+    write : (bytes -> int -> int -> unit) -> 'a -> unit;
+}
+
+external id : 'a -> 'a = "%identity"
+
+let header_size = 8
+
+let max_int_int64 = Int64.of_int max_int
+let min_int_int64 = Int64.of_int min_int
+
+let data_size header offset =
+  if offset + header_size <= Bytes.length header then
+    let n = Bytes.get_int64_be header offset in
+    if n < 0L then
+      raise (Error "data_size: negative size")
+    else if n <= max_int_int64 then
+      Int64.to_int n
+    else
+      raise (Error "data_size: payload too large")
+  else
+    raise (Error "data_size: header too short")
+
+let to_string m x =
+  let buffer = Buffer.create 1024 in
+  m.write (Buffer.add_subbytes buffer) x;
+  let header = Bytes.create header_size in
+  Bytes.set_int64_be header 0 (Int64.of_int (Buffer.length buffer));
+  Bytes.to_string header ^ Buffer.contents buffer
+
+let from_bytes m buffer offset =
+  let length = Bytes.length buffer in
+  let offset = ref (offset + header_size) in
+  m.read (fun buffer' offset' n ->
+      let i = !offset in
+      if i + n <= length then (
+        offset := i + n;
+        Bytes.blit buffer i buffer' offset' n
+      ) else (
+        raise (Error "from_bytes: end of input")
+      )
+    )
+
+let from_string m buffer offset =
+  from_bytes m (Bytes.of_string buffer) offset
+
+let from_channel m ic =
+  let header = Bytes.create header_size in
+  really_input ic header 0 header_size;
+  m.read (really_input ic)
+
+let to_channel m oc x =
+  let header = Bytes.create header_size in
+  let header_pos = pos_out oc in
+  output oc header 0 header_size;
+  m.write (output oc) x;
+  let end_pos = pos_out oc in
+  let data_size = end_pos - header_pos - header_size in
+  Bytes.set_int64_be header 0 (Int64.of_int data_size);
+  seek_out oc header_pos;
+  output oc header 0 header_size;
+  seek_out oc end_pos
+
+let rec1 a =
+  let rec fa =
+    {
+      read = (fun recv -> (a fa).read recv);
+      write = (fun send x -> (a fa).write send x);
+    }
+  in
+  fa
+
+let rec2 a b =
+  let rec fa =
+    {
+      read = (fun recv -> (a fb).read recv);
+      write = (fun send x -> (a fb).write send x);
+    }
+  and fb =
+    {
+      read = (fun recv -> (b fa).read recv);
+      write = (fun send x -> (b fa).write send x);
+    }
+  in
+  (fb, fa)
+
+let unit =
+  {
+    read = (fun _ -> ());
+    write = (fun _ () -> ());
+  }
+
+let char =
+  {
+    read =
+      (fun recv ->
+        let buffer = Bytes.create 1 in
+        recv buffer 0 1;
+        Bytes.unsafe_get buffer 0
+      );
+    write =
+      (fun send x ->
+        let res = Bytes.create 1 in
+        Bytes.unsafe_set res 0 x;
+        send res 0 1
+      );
+  }
+
+let bool =
+  {
+    read =
+      (fun recv ->
+        match char.read recv with
+        | '\000' -> false
+        | '\001' -> true
+        | _ -> raise (Error "bool: invalid value")
+      );
+    write =
+      (fun send x ->
+        char.write send (if x then '\001' else '\000')
+      );
+  }
+
+let int32 =
+  {
+    read =
+      (fun recv ->
+        let buffer = Bytes.create 4 in
+        recv buffer 0 4;
+        Bytes.get_int32_be buffer 0
+      );
+    write =
+      (fun send x ->
+        let res = Bytes.create 4 in
+        Bytes.set_int32_be res 0 x;
+        send res 0 4
+      );
+  }
+
+let int64 =
+  {
+    read =
+      (fun recv ->
+        let realize n get of_int =
+          let buffer = Bytes.create n in
+          recv buffer 0 n;
+          of_int (get buffer 0)
+        in
+        match int_of_char (char.read recv) with
+        | 0 -> 0L
+        | 1 -> realize 1 Bytes.get_int8 Int64.of_int
+        | 2 -> realize 2 Bytes.get_int16_be Int64.of_int
+        | 4 -> realize 4 Bytes.get_int32_be Int64.of_int32
+        | 8 -> realize 8 Bytes.get_int64_be id
+        | n -> raise (Error (Printf.sprintf "int64.read: unexpected size (%d)" n))
+      );
+    write =
+      (fun send x ->
+        let realize n set to_int =
+          let buffer = Bytes.create (1 + n) in
+          Bytes.unsafe_set buffer 0 (char_of_int n);
+          set buffer 1 (to_int x);
+          send buffer 0 (1 + n)
+        in
+        if x = 0L then
+          char.write send '\000'
+        else if -0x80L <= x && x < 0x80L then
+          realize 1 Bytes.set_int8 Int64.to_int
+        else if -0x8000L <= x && x < 0x8000L then
+          realize 2 Bytes.set_int16_be Int64.to_int
+        else if -0x8000_0000L <= x && x < 0x8000_0000L then
+          realize 4 Bytes.set_int32_be Int64.to_int32
+        else
+          realize 8 Bytes.set_int64_be id
+      );
+  }
+
+let int =
+  {
+    read =
+      (fun recv ->
+        let r = int64.read recv in
+        if r < min_int_int64 || r > max_int_int64 then
+          raise (Error "int.read: too large")
+        else
+          Int64.to_int r
+      );
+    write =
+      (fun send x ->
+        int64.write send (Int64.of_int x)
+      );
+  }
+
+let string =
+  {
+    read =
+      (fun recv ->
+        let length = int.read recv in
+        let buffer = Bytes.create length in
+        recv buffer 0 length;
+        Bytes.to_string buffer
+      );
+    write =
+      (fun send x ->
+        let length = String.length x in
+        int.write send length;
+        send (Bytes.of_string x) 0 length
+      );
+  }
+
+type bytearray =
+  (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
+
+external unsafe_blit_from_bytes : bytes -> int -> bytearray -> int -> int -> unit
+  = "ml_blit_bytes_to_bigarray" [@@noalloc]
+
+external unsafe_blit_to_bytes : bytearray -> int -> bytes -> int -> int -> unit
+  = "ml_blit_bigarray_to_bytes" [@@noalloc]
+
+let bytearray =
+  {
+    read =
+      (fun recv ->
+        let length = int.read recv in
+        let res = Bigarray.(Array1.create char c_layout length) in
+        let rec loop offset length =
+          if length > 0 then (
+            let sub_length = min length Sys.max_string_length in
+            let buffer = Bytes.create sub_length in
+            recv buffer 0 sub_length;
+            unsafe_blit_from_bytes buffer 0 res offset sub_length;
+            loop (offset + sub_length) (length - sub_length)
+          )
+        in
+        loop 0 length;
+        res
+      );
+    write =
+      (fun send x ->
+        let length = Bigarray.Array1.dim x in
+        int.write send length;
+        let buffer = Bytes.create (min length Sys.max_string_length) in
+        let rec loop offset length =
+          if length > 0 then (
+            let sub_length = min length Sys.max_string_length in
+            unsafe_blit_to_bytes x offset buffer 0 sub_length;
+            send buffer 0 sub_length;
+            loop (offset + sub_length) (length - sub_length)
+          )
+        in
+        loop 0 length
+      );
+  }
+
+let marshal_to_bytearray m x =
+  let data_size = ref 0 in
+  m.write (fun _ _ length -> data_size := !data_size + length) x;
+  let header = Bytes.create header_size in
+  Bytes.set_int64_be header 0 (Int64.of_int !data_size);
+  let total_size = header_size + !data_size in
+  let result = Bigarray.(Array1.create char c_layout total_size) in
+  unsafe_blit_from_bytes header 0 result 0 header_size;
+  let offset = ref header_size in
+  m.write (fun buffer offset' length ->
+      let i = !offset in
+      if i + length <= total_size then (
+        unsafe_blit_from_bytes buffer offset' result i length;
+        offset := i + length
+      ) else (
+        raise (Error "marshal_to_bytearray: length inconsistency")
+      )
+    ) x;
+  if !offset <> total_size then
+    raise (Error "marshal_to_bytearray: universe inconsistency");
+  result
+
+let unmarshal_from_bytearray m x offset =
+  let length = Bigarray.Array1.dim x in
+  let offset = ref (offset + header_size) in
+  m.read (fun buffer' offset' n ->
+      let i = !offset in
+      if i + n <= length then (
+        offset := i + n;
+        unsafe_blit_to_bytes x i buffer' offset' n
+      ) else (
+        raise (Error "unmarshal_from_bytearray: end of input")
+      )
+    )
+
+type int32bigarray =
+  (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t
+
+let int32bigarray =
+  {
+    read =
+      (fun recv ->
+        let length = int.read recv in
+        let res = Bigarray.(Array1.create int32 c_layout length) in
+        for i = 0 to length - 1 do
+          res.{i} <- int32.read recv
+        done;
+        res
+      );
+    write =
+      (fun send x ->
+        let length = Bigarray.Array1.dim x in
+        int.write send length;
+        for i = 0 to length - 1 do
+          int32.write send x.{i}
+        done
+      );
+  }
+
+let float =
+  {
+    read =
+      (fun recv ->
+        Int64.float_of_bits (int64.read recv)
+      );
+    write =
+      (fun send x ->
+        int64.write send (Int64.bits_of_float x)
+      );
+  }
+
+let list m =
+  {
+    read =
+      (fun recv ->
+        let length = int.read recv in
+        let result = ref [] in
+        for _ = 1 to length do
+          result := m.read recv :: !result
+        done;
+        List.rev !result
+      );
+    write =
+      (fun send x ->
+        int.write send (List.length x);
+        List.iter (fun x -> m.write send x) x
+      );
+  }
+
+let prod2 ma mb f g =
+  {
+    read =
+      (fun recv ->
+        let a = ma.read recv in
+        let b = mb.read recv in
+        g (a, b)
+      );
+    write =
+      (fun send x ->
+        let a, b = f x in
+        ma.write send a;
+        mb.write send b
+      );
+  }
+
+let prod3 ma mb mc f g =
+  {
+    read =
+      (fun recv ->
+        let a = ma.read recv in
+        let b = mb.read recv in
+        let c = mc.read recv in
+        g (a, b, c)
+      );
+    write =
+      (fun send x ->
+        let a, b, c = f x in
+        ma.write send a;
+        mb.write send b;
+        mc.write send c
+      );
+  }
+
+let prod4 ma mb mc md f g =
+  {
+    read =
+      (fun recv ->
+        let a = ma.read recv in
+        let b = mb.read recv in
+        let c = mc.read recv in
+        let d = md.read recv in
+        g (a, b, c, d)
+      );
+    write =
+      (fun send x ->
+        let a, b, c, d = f x in
+        ma.write send a;
+        mb.write send b;
+        mc.write send c;
+        md.write send d
+      );
+  }
+
+let prod5 ma mb mc md me f g =
+  {
+    read =
+      (fun recv ->
+        let a = ma.read recv in
+        let b = mb.read recv in
+        let c = mc.read recv in
+        let d = md.read recv in
+        let e = me.read recv in
+        g (a, b, c, d, e)
+      );
+    write =
+      (fun send x ->
+        let a, b, c, d, e = f x in
+        ma.write send a;
+        mb.write send b;
+        mc.write send c;
+        md.write send d;
+        me.write send e
+      );
+  }
+
+let prod6 ma mb mc md me mf f g =
+  {
+    read =
+      (fun recv ->
+        let a = ma.read recv in
+        let b = mb.read recv in
+        let c = mc.read recv in
+        let d = md.read recv in
+        let e = me.read recv in
+        let f = mf.read recv in
+        g (a, b, c, d, e, f)
+      );
+    write =
+      (fun send x ->
+        let a, b, c, d, e, f = f x in
+        ma.write send a;
+        mb.write send b;
+        mc.write send c;
+        md.write send d;
+        me.write send e;
+        mf.write send f
+      );
+  }
+
+let sum1 ma f g =
+  {
+    read = (fun recv -> g (ma.read recv));
+    write = (fun send x -> ma.write send (f x));
+  }
+
+type ('a, 'b) sum2 = I21 of 'a | I22 of 'b
+
+let sum2 ma mb f g =
+  {
+    read =
+      (fun recv ->
+        g (match char.read recv with
+           | '\000' -> I21 (ma.read recv)
+           | '\001' -> I22 (mb.read recv)
+           | _ -> raise (Error "sum2: invalid tag"))
+      );
+    write =
+      (fun send x ->
+        match f x with
+        | I21 a -> char.write send '\000'; ma.write send a
+        | I22 a -> char.write send '\001'; mb.write send a
+      );
+  }
+
+let option m =
+  sum2 unit m
+    (function
+     | None -> I21 ()
+     | Some a -> I22 a)
+    (function
+     | I21 () -> None
+     | I22 a -> Some a)
+
+type ('a, 'b, 'c) sum3 = I31 of 'a | I32 of 'b | I33 of 'c
+
+let sum3 ma mb mc f g =
+  {
+    read =
+      (fun recv ->
+        g (match char.read recv with
+           | '\000' -> I31 (ma.read recv)
+           | '\001' -> I32 (mb.read recv)
+           | '\002' -> I33 (mc.read recv)
+           | _ -> raise (Error "sum3: invalid tag"))
+      );
+    write =
+      (fun send x ->
+        match f x with
+        | I31 a -> char.write send '\000'; ma.write send a
+        | I32 a -> char.write send '\001'; mb.write send a
+        | I33 a -> char.write send '\002'; mc.write send a
+      );
+  }
+
+type ('a, 'b, 'c, 'd) sum4 = I41 of 'a | I42 of 'b | I43 of 'c | I44 of 'd
+
+let sum4 ma mb mc md f g =
+  {
+    read =
+      (fun recv ->
+        g (match char.read recv with
+           | '\000' -> I41 (ma.read recv)
+           | '\001' -> I42 (mb.read recv)
+           | '\002' -> I43 (mc.read recv)
+           | '\003' -> I44 (md.read recv)
+           | _ -> raise (Error "sum4: invalid tag"))
+      );
+    write =
+      (fun send x ->
+        match f x with
+        | I41 a -> char.write send '\000'; ma.write send a
+        | I42 a -> char.write send '\001'; mb.write send a
+        | I43 a -> char.write send '\002'; mc.write send a
+        | I44 a -> char.write send '\003'; md.write send a
+      );
+  }
+
+type ('a, 'b, 'c, 'd, 'e) sum5 = I51 of 'a | I52 of 'b | I53 of 'c | I54 of 'd | I55 of 'e
+
+let sum5 ma mb mc md me f g =
+  {
+    read =
+      (fun recv ->
+        g (match char.read recv with
+           | '\000' -> I51 (ma.read recv)
+           | '\001' -> I52 (mb.read recv)
+           | '\002' -> I53 (mc.read recv)
+           | '\003' -> I54 (md.read recv)
+           | '\004' -> I55 (me.read recv)
+           | _ -> raise (Error "sum5: invalid tag"))
+      );
+    write =
+      (fun send x ->
+        match f x with
+        | I51 a -> char.write send '\000'; ma.write send a
+        | I52 a -> char.write send '\001'; mb.write send a
+        | I53 a -> char.write send '\002'; mc.write send a
+        | I54 a -> char.write send '\003'; md.write send a
+        | I55 a -> char.write send '\004'; me.write send a
+      );
+  }
+
+type ('a, 'b, 'c, 'd, 'e, 'f) sum6 = I61 of 'a | I62 of 'b | I63 of 'c | I64 of 'd | I65 of 'e | I66 of 'f
+
+let sum6 ma mb mc md me mf f g =
+  {
+    read =
+      (fun recv ->
+        g (match char.read recv with
+           | '\000' -> I61 (ma.read recv)
+           | '\001' -> I62 (mb.read recv)
+           | '\002' -> I63 (mc.read recv)
+           | '\003' -> I64 (md.read recv)
+           | '\004' -> I65 (me.read recv)
+           | '\005' -> I66 (mf.read recv)
+           | _ -> raise (Error "sum6: invalid tag"))
+      );
+    write =
+      (fun send x ->
+        match f x with
+        | I61 a -> char.write send '\000'; ma.write send a
+        | I62 a -> char.write send '\001'; mb.write send a
+        | I63 a -> char.write send '\002'; mc.write send a
+        | I64 a -> char.write send '\003'; md.write send a
+        | I65 a -> char.write send '\004'; me.write send a
+        | I66 a -> char.write send '\005'; mf.write send a
+      );
+  }
+
+let cond c d m =
+  {
+    read =
+      (fun recv ->
+        if c () then m.read recv else d
+      );
+    write =
+      (fun send x ->
+        if c () then m.write send x else ()
+      );
+  }
+
+module type PROPLIST_S = sig
+  type key = string
+  type value = Obj.t
+  type map
+  val cardinal : map -> int
+  val empty : map
+  val add : key -> value -> map -> map
+  val iter : (key -> value -> unit) -> map -> unit
+  val find_m : key -> value t
+end
+
+module Proplist (S : PROPLIST_S) = struct
+  let m =
+    {
+      read =
+        (fun recv ->
+          let length = int.read recv in
+          let res = ref S.empty in
+          for _ = 1 to length do
+            let key = string.read recv in
+            let value = (S.find_m key).read recv in
+            res := S.add key value !res
+          done;
+          !res
+        );
+      write =
+        (fun send x ->
+          let length = S.cardinal x in
+          int.write send length;
+          S.iter (fun key value ->
+              string.write send key;
+              (S.find_m key).write send value
+            ) x
+        );
+    }
+end
diff --git a/src/ubase/umarshal.mli b/src/ubase/umarshal.mli
new file mode 100644
index 0000000..4cb5c80
--- /dev/null
+++ b/src/ubase/umarshal.mli
@@ -0,0 +1,118 @@
+(* Unison file synchronizer: src/ubase/umarshal.mli *)
+(* Copyright 2020, Stéphane Glondu (see COPYING for details) *)
+
+(* The purpose and characteristics of the Umarshal module are not unlike
+   those of Stdlib's Marshal module, with two main differences
+
+    - it is not intended to encode and decode arbitrary data structures.
+    - it is intended to be compatible and stable across all machines and
+      OCaml versions.
+
+   This module provides basic infrastructure for marshaling along with some
+   combinators to build marshaling functions for various data structures.
+
+   The encoding format used by Umarshal is not following any standard, it
+   is a minimal binary format with no overhead designed to encode a limited
+   set of data types in a platform-neutral way. The format does not carry
+   any schema or other type information. *)
+
+exception Error of string
+
+(* Type ['a t] defines un-/marshaling functions for a data structure with
+   type ['a].
+
+   The combinators in this module are used to build [Umarshal.t] values.
+   Combinators are provided for basic types, such as ints, floats and
+   strings, for basic structures such as products (tuples) and sums
+   (variants), and for recursive structures. Other types (such as records)
+   will need to be converted to tuples. All sum and product combinators
+   receive conversion functions for this purpose.
+
+   By convention, a [Umarshal.t] value for a type in another module will be
+   named by the name of the type prefixed with "m", or just "m" in case the
+   type is named "t".
+
+   For example (in other modules):
+     [mtyp : typ Umarshal.t]
+     [m : t Umarshal.t]
+*)
+type 'a t
+
+external id : 'a -> 'a = "%identity"
+
+val header_size : int
+val data_size : bytes -> int -> int
+
+val to_string : 'a t -> 'a -> string
+val from_bytes : 'a t -> bytes -> int -> 'a
+val from_string : 'a t -> string -> int -> 'a
+
+val from_channel : 'a t -> in_channel -> 'a
+val to_channel : 'a t -> out_channel -> 'a -> unit
+
+val rec1 : ('a t -> 'a t) -> 'a t
+val rec2 : ('a t -> 'b t) -> ('b t -> 'a t) -> 'a t * 'b t
+
+val unit : unit t
+val bool : bool t
+val int : int t
+val int64 : int64 t
+val float : float t
+
+val string : string t
+
+type bytearray =
+  (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t
+
+val bytearray : bytearray t
+
+val marshal_to_bytearray : 'a t -> 'a -> bytearray
+val unmarshal_from_bytearray : 'a t -> bytearray -> int -> 'a
+
+type int32bigarray =
+  (int32, Bigarray.int32_elt, Bigarray.c_layout) Bigarray.Array1.t
+
+val int32bigarray : int32bigarray t
+
+val option : 'a t -> 'a option t
+val list : 'a t -> 'a list t
+
+val prod2 : 'a t -> 'b t -> ('r -> 'a * 'b) -> ('a * 'b -> 'r) -> 'r t
+val prod3 : 'a t -> 'b t -> 'c t -> ('r -> 'a * 'b * 'c) -> ('a * 'b * 'c-> 'r) -> 'r t
+val prod4 : 'a t -> 'b t -> 'c t -> 'd t -> ('r -> 'a * 'b * 'c * 'd) -> ('a * 'b * 'c * 'd -> 'r) -> 'r t
+val prod5 : 'a t -> 'b t -> 'c t -> 'd t -> 'e t -> ('r -> 'a * 'b * 'c * 'd * 'e) -> ('a * 'b * 'c * 'd * 'e -> 'r) -> 'r t
+val prod6 : 'a t -> 'b t -> 'c t -> 'd t -> 'e t -> 'f t -> ('r -> 'a * 'b * 'c * 'd * 'e * 'f) -> ('a * 'b * 'c * 'd * 'e * 'f -> 'r) -> 'r t
+
+val sum1 : 'a t -> ('r -> 'a) -> ('a -> 'r) -> 'r t
+
+type ('a, 'b) sum2 = I21 of 'a | I22 of 'b
+val sum2 : 'a t -> 'b t -> ('r -> ('a, 'b) sum2) -> (('a, 'b) sum2 -> 'r) -> 'r t
+
+type ('a, 'b, 'c) sum3 = I31 of 'a | I32 of 'b | I33 of 'c
+val sum3 : 'a t -> 'b t -> 'c t -> ('r -> ('a, 'b, 'c) sum3) -> (('a, 'b, 'c) sum3 -> 'r) -> 'r t
+
+type ('a, 'b, 'c, 'd) sum4 = I41 of 'a | I42 of 'b | I43 of 'c | I44 of 'd
+val sum4 : 'a t -> 'b t -> 'c t -> 'd t -> ('r -> ('a, 'b, 'c, 'd) sum4) -> (('a, 'b, 'c, 'd) sum4 -> 'r) -> 'r t
+
+type ('a, 'b, 'c, 'd, 'e) sum5 = I51 of 'a | I52 of 'b | I53 of 'c | I54 of 'd | I55 of 'e
+val sum5 : 'a t -> 'b t -> 'c t -> 'd t -> 'e t -> ('r -> ('a, 'b, 'c, 'd, 'e) sum5) -> (('a, 'b, 'c, 'd, 'e) sum5 -> 'r) -> 'r t
+
+type ('a, 'b, 'c, 'd, 'e, 'f) sum6 = I61 of 'a | I62 of 'b | I63 of 'c | I64 of 'd | I65 of 'e | I66 of 'f
+val sum6 : 'a t -> 'b t -> 'c t -> 'd t -> 'e t -> 'f t -> ('r -> ('a, 'b, 'c, 'd, 'e, 'f) sum6) -> (('a, 'b, 'c, 'd, 'e, 'f) sum6 -> 'r) -> 'r t
+
+val cond : (unit -> bool) -> 'a -> 'a t -> 'a t
+
+module type PROPLIST_S = sig
+  type key = string
+  type value = Obj.t
+  type map
+  val cardinal : map -> int
+  val empty : map
+  val add : key -> value -> map -> map
+  val iter : (key -> value -> unit) -> map -> unit
+  val find_m : key -> value t
+end
+
+module Proplist (S : PROPLIST_S) : sig
+  val m : S.map t
+end
diff --git a/src/ubase/util.ml b/src/ubase/util.ml
index 0223734..c5bbfde 100644
--- a/src/ubase/util.ml
+++ b/src/ubase/util.ml
@@ -57,19 +57,23 @@ let stringSetFromList l =
 (*                    Debugging / error messages                             *)
 (*****************************************************************************)
 
-let infos = ref ""
+type infos = { s : string; clr : string }
+let infos = ref { s = ""; clr = "" }
 
 let clear_infos () =
-  if !infos <> "" then begin
+  if !infos.clr <> "" then begin
+    print_string !infos.clr;
+    flush stdout
+  end else if !infos.s <> "" then begin
     print_string "\r";
-    print_string (String.make (String.length !infos) ' ');
+    print_string (String.make (String.length !infos.s) ' ');
     print_string "\r";
     flush stdout
   end
 let show_infos () =
-  if !infos <> "" then begin print_string !infos; flush stdout end
-let set_infos s =
-  if s <> !infos then begin clear_infos (); infos := s; show_infos () end
+  if !infos.s <> "" then begin print_string !infos.s; flush stdout end
+let set_infos ?(clr = "") s =
+  if s <> !infos.s then begin clear_infos (); infos := {s; clr}; show_infos () end
 
 let msg f =
   clear_infos ();
@@ -136,7 +140,7 @@ let encodeException m kind e =
     Unix.Unix_error(err,fnname,param) ->
       let s =   "Error in " ^ m ^ ":\n"
               ^ (Unix.error_message err)
-              ^ " [" ^ fnname ^ "(" ^ param ^ ")]%s" ^
+              ^ " [" ^ fnname ^ "(" ^ param ^ ")]" ^
               (match err with
                  Unix.EUNKNOWNERR n -> Format.sprintf " (code %d)" n
                | _                  -> "")
@@ -272,6 +276,19 @@ let process_status_to_string = function
   | Unix.WSIGNALED i -> Printf.sprintf "Killed by signal %d" i
   | Unix.WSTOPPED i  -> Printf.sprintf "Stopped by signal %d" i
 
+
+let blockSignals sigs f =
+  let (prevMask, ok) =
+    try (Unix.sigprocmask SIG_BLOCK sigs, true)
+    with Invalid_argument _ -> ([], false) in
+  let restoreMask () =
+    if ok then Unix.sigprocmask SIG_SETMASK prevMask |> ignore in
+  try let r = f () in restoreMask (); r
+  with e ->
+    let origbt = Printexc.get_raw_backtrace () in
+    restoreMask ();
+    Printexc.raise_with_backtrace e origbt
+
 (*****************************************************************************)
 (*                         OS TYPE                                           *)
 (*****************************************************************************)
@@ -328,6 +345,23 @@ let percentageOfTotal current total =
 
 let percent2string p = Printf.sprintf "%3d%%" (truncate (max 0. (min 100. p)))
 
+let gib = 1073741824.
+let mib = 1048576.
+let kib = 1024.
+let bytes2string v =
+  if v > 1_048_051_711L then
+    Printf.sprintf "%.2f GiB" (Int64.to_float v /. gib)
+  else if v > 104_805_171L then
+    Printf.sprintf "%.0f MiB" (Int64.to_float v /. mib)
+  else if v > 1_023_487L then
+    Printf.sprintf "%.1f MiB" (Int64.to_float v /. mib)
+  else if v > 102_348L then
+    Printf.sprintf "%.0f KiB" (Int64.to_float v /. kib)
+  else if v > 999L then
+    Printf.sprintf "%.1f KiB" (Int64.to_float v /. kib)
+  else
+    Printf.sprintf "%Ld B" v
+
 let extractValueFromOption = function
     None -> raise (Fatal "extractValueFromOption failed")
   | Some(v) -> v
@@ -461,7 +495,7 @@ let padto n s = s ^ (String.make (max 0 (n - String.length s)) ' ')
 (*              Building pathnames in the user's home dir                    *)
 (*****************************************************************************)
 
-let homeDirStr = 
+let homeDir () =
     (if (osType = `Unix) || isCygwin then
        safeGetenv "HOME"
      else if osType = `Win32 then
@@ -482,10 +516,7 @@ let homeDirStr =
      else
        assert false (* osType can't be anything else *))
 
-let homeDir () =
-  System.fspathFromString homeDirStr
-
-let fileInHomeDir n = System.fspathConcat (homeDir ()) n
+let fileInHomeDir n = Filename.concat (homeDir ()) n
 
 (*****************************************************************************)
 (*                       .unison dir                                         *)
@@ -497,7 +528,7 @@ let isMacOSX = isMacOSXPred ()
 
 let unisonDir =
   try
-    System.fspathFromString (System.getenv "UNISON")
+    System.getenv "UNISON"
   with Not_found ->
     let genericName =
       fileInHomeDir (Printf.sprintf ".%s" ProjectInfo.myName) in
@@ -506,11 +537,9 @@ let unisonDir =
     else
       genericName
 
-let unisonDirStr = System.fspathToString unisonDir
-
-let fileInUnisonDir str = System.fspathConcat unisonDir str
+let fileInUnisonDir str = Filename.concat unisonDir str
 
 let fileMaybeRelToUnisonDir n =
   if Filename.is_relative n
   then fileInUnisonDir n
-  else System.fspathFromString n
+  else n
diff --git a/src/ubase/util.mli b/src/ubase/util.mli
index ed636c2..dae2b70 100644
--- a/src/ubase/util.mli
+++ b/src/ubase/util.mli
@@ -33,6 +33,11 @@ val printException : exn -> string
 
 val process_status_to_string : Unix.process_status -> string
 
+(* [blockSignals sigs f] blocks signals [sigs] (if supported by OS),
+   executes [f ()] and restores the original signal mask before returning
+   the result of executing [f ()] (value or exception). *)
+val blockSignals : int list -> (unit -> 'a) -> 'a
+
 (* ---------------------------------------------------------------------- *)
 (* Strings *)
 
@@ -83,6 +88,7 @@ val percentageOfTotal :
   int        (* percentage of total *)
 val monthname : int -> string
 val percent2string : float -> string
+val bytes2string : int64 -> string
 
 (* Just like the versions in the Unix module, but raising Transient
    instead of Unix_error *)
@@ -100,14 +106,13 @@ val debug : string -> (unit->unit) -> unit
 val warnPrinter : (string -> unit) option ref
 val warn : string -> unit
 
-(* Gives the fspath of the archive directory on the machine, depending on    *)
+(* Gives the path of the archive directory on the machine, depending on      *)
 (* which OS we use                                                           *)
-val unisonDir : System.fspath
+val unisonDir : string
 
-(* build a fspath representing an archive child path whose name is given     *)
-val fileInUnisonDir : string -> System.fspath
-val fileMaybeRelToUnisonDir : string -> System.fspath
-val unisonDirStr : string
+(* build a path representing an archive child path whose name is given       *)
+val fileInUnisonDir : string -> string
+val fileMaybeRelToUnisonDir : string -> string
 
 (* Printing and formatting functions *)
 
@@ -125,5 +130,6 @@ val format_to_string : (unit -> unit) -> string
    flush the stream after each one *)
 val msg : ('a, out_channel, unit) format -> 'a
 
-(* Set the info line *)
-val set_infos : string -> unit
+(* Set the info line.
+   [~clr] is an alternative clear sequence to clear this info only. *)
+val set_infos : ?clr:string -> string -> unit
diff --git a/src/uicommon.ml b/src/uicommon.ml
index 04bb134..94fbc66 100644
--- a/src/uicommon.ml
+++ b/src/uicommon.ml
@@ -26,6 +26,15 @@ type interface =
    Text
  | Graphic
 
+let minterface =
+  Umarshal.(sum2 unit unit
+              (function
+               | Text -> I21 ()
+               | Graphic -> I22 ())
+              (function
+               | I21 () -> Text
+               | I22 () -> Graphic))
+
 module type UI =
 sig
  val start : interface -> unit
@@ -38,7 +47,9 @@ end
  **********************************************************************)
 
 let auto =
-  Prefs.createBool "auto" false "automatically accept default (nonconflicting) actions"
+  Prefs.createBool "auto" false
+    ~category:(`Basic `Syncprocess_CLI)
+    "automatically accept default (nonconflicting) actions"
     ("When set to {\\tt true}, this flag causes the user "
      ^ "interface to skip asking for confirmations on "
      ^ "non-conflicting changes.  (More precisely, when the user interface "
@@ -50,17 +61,20 @@ let auto =
    gets sent to the server at startup *)
 let mainWindowHeight =
   Prefs.createInt "height" 15
-    "!height (in lines) of main window in graphical interface"
+    ~category:(`Advanced `GUI)
+    "height (in lines) of main window in graphical interface"
     ("Used to set the height (in lines) of the main window in the graphical "
      ^ "user interface.")
 
 let expert =
   Prefs.createBool "expert" false
+    ~category:(`Internal `Devel)
     "*Enable some developers-only functionality in the UI" ""
 
 let profileLabel =
   Prefs.createString "label" ""
-    "!provide a descriptive string label for this profile"
+    ~category:(`Advanced `General)
+    "provide a descriptive string label for this profile"
     ("Used in a profile to provide a descriptive string documenting its "
      ^ "settings.  (This is useful for users that switch between several "
      ^ "profiles, especially using the `fast switch' feature of the "
@@ -68,7 +82,8 @@ let profileLabel =
 
 let profileKey =
   Prefs.createString "key" ""
-    "!define a keyboard shortcut for this profile (in some UIs)"
+    ~category:(`Advanced `General)
+    "define a keyboard shortcut for this profile (in some UIs)"
     ("Used in a profile to define a numeric key (0-9) that can be used in "
      ^ "the user interface to switch immediately to this profile.")
 (* This preference is not actually referred to in the code anywhere, since
@@ -78,7 +93,8 @@ let profileKey =
 
 let contactquietly =
   Prefs.createBool "contactquietly" false
-    "!suppress the 'contacting server' message during startup"
+    ~category:(`Advanced `General)
+    "suppress the 'contacting server' message during startup"
     ("If this flag is set, Unison will skip displaying the "
      ^ "`Contacting server' message (which some users find annoying) "
      ^ "during startup.")
@@ -87,19 +103,52 @@ let contactingServerMsg () =
   Printf.sprintf "Unison %s: Contacting server..." Uutil.myVersion 
 
 let repeat =
-  Prefs.createString "repeat" ""
-    "!synchronize repeatedly (text interface only)"
+  let parseRepeat s =
+    let parseTime ts =
+      try int_of_string ts with Failure _ ->
+        raise (Prefs.IllegalValue ("Value of 'repeat' preference ("
+          ^ s ^ ") should be either a number, 'watch' or 'watch+<number>'"))
+    in
+    let nonBlankLower x =
+      match String.trim x with "" -> None | s -> Some (String.lowercase_ascii s)
+    in
+    try
+      match Safelist.filterMap nonBlankLower (String.split_on_char '+' s) with
+      | [] -> `NoRepeat
+      | ["watch"] -> `Watch
+      | ["watch"; i] | [i; "watch"] -> `WatchAndInterval (parseTime i)
+      | _ -> `Interval (parseTime s)
+    with
+    | Prefs.IllegalValue _ as e -> `Invalid (s, e)
+  in
+  let externRepeat = function
+    | `NoRepeat | `Invalid _ -> ""
+    | `Watch -> "watch"
+    | `WatchAndInterval i -> "watch+" ^ (string_of_int i)
+    | `Interval i -> string_of_int i
+  in
+  Prefs.create "repeat" `NoRepeat
+    ~category:(`Advanced `Syncprocess_CLI)
+    "synchronize repeatedly (text interface only)"
     ("Setting this preference causes the text-mode interface to synchronize "
      ^ "repeatedly, rather than doing it just once and stopping.  If the "
      ^ "argument is a number, Unison will pause for that many seconds before "
      ^ "beginning again. When the argument is \\verb|watch|, Unison relies on "
      ^ "an external file monitoring process to synchronize whenever a change "
-     ^ "happens.")
-let repeatWatcher () = Prefs.read repeat = "watch"
+     ^ "happens.  You can combine the two with a \\verb|+| character to use "
+     ^ "file monitoring and also do a full scan every specificed number of "
+     ^ "seconds.  For example, \\verb|watch+3600| will react to changes "
+     ^ "immediately and additionally do a full scan every hour.")
+    (fun _ -> parseRepeat)
+    (fun r -> [externRepeat r])
+    Umarshal.(sum1 string externRepeat parseRepeat)
+let repeatWatcher () =
+  match Prefs.read repeat with `Watch | `WatchAndInterval _ -> true | _ -> false
 
 let retry =
   Prefs.createInt "retry" 0
-    "!re-try failed synchronizations N times (text ui only)"
+    ~category:(`Advanced `Syncprocess_CLI)
+    "re-try failed synchronizations N times (text ui only)"
     ("Setting this preference causes the text-mode interface to try again "
      ^ "to synchronize "
      ^ "updated paths where synchronization fails.  Each such path will be "
@@ -108,7 +157,8 @@ let retry =
 
 let confirmmerge =
   Prefs.createBool "confirmmerge" false
-    "!ask for confirmation before committing results of a merge"
+    ~category:(`Advanced `Syncprocess)
+    "ask for confirmation before committing results of a merge"
     ("Setting this preference causes both the text and graphical interfaces"
      ^ " to ask the user if the results of a merge command may be committed "
      ^ " to the replica or not. Since the merge command works on temporary files,"
@@ -119,7 +169,9 @@ let confirmmerge =
 let runTestsPrefName = "selftest"
 let runtests =
   Prefs.createBool runTestsPrefName false
-    "!run internal tests and exit"
+    ~category:`Expert
+    ~cli_only:true
+    "run internal tests and exit"
    ("Run internal tests and exit.  This option is mostly for developers and must be used "
   ^ "carefully: in particular, "
   ^ "it will delete the contents of both roots, so that it can install its own files "
@@ -146,6 +198,7 @@ let choose s1 s2 = if !Update.foundArchives then s1 else s2
 
 let showprev =
   Prefs.createBool "showprev" false
+    ~category:(`Internal `Devel)
     "*Show previous properties, if they differ from current"
     ""
 
@@ -413,6 +466,282 @@ let addIgnorePattern theRegExp =
      example, we do a "rescan") *)
   Lwt_unix.run (Globals.propagatePrefs ())
 
+(**********************************************************************
+                     Statistics for update progress
+ **********************************************************************)
+
+(* This seemingly very complex code for calculating the progress rate
+   and ETA has partly to do with Unison currently not tracking progress
+   very accurately. Several potentially very time-consuming operations
+   are not tracked at all: hashing files before and after the copy, for
+   example. The entire amount of work may not even be known in advance
+   when continuing partial transfers after the previous sync has been
+   interrupted. This makes it very difficult to provide meaningful rate
+   and ETA information. The code below is the current best approximation.
+   The way to simplify this code here is to first and foremost improve
+   progress tracking and reporting. *)
+
+module Stats = struct
+
+let calcETA rem rate =
+  if Float.is_nan rate || Float.is_nan rem || rem < 0. then "" else
+  let t = truncate (rem /. rate +. 0.5) in
+  (* Estimating the remaining time is not accurate. Reduce the display
+     precision (and reduce more when longer time remaining). *)
+  let h, (m, sec) =
+    if t >= 3420 then
+      let u = t + 180 in u / 3600, (((u mod 3600) / 300) * 5, 0)
+    else
+      0,
+      if t >= 2640 then ((t + 180) / 300) * 5, 0
+      else if t >= 1800 then ((t + 119) / 120) * 2, 0
+      else if t >= 120 then let u = t + 15 in u / 60, ((u mod 60) / 30) * 30
+      else t / 60, t mod 60
+  in
+  Printf.sprintf "%02d:%02d:%02d" h m sec
+
+let movAvg curr prev ?(c = 1.) deltaTime avgPeriod =
+  if Float.is_nan prev then curr else
+  let a = c *. Float.min (1. -. exp (-. deltaTime /. avgPeriod)) 1. in
+  (* Simplified from a *. curr +. (1. -. a) *. prev *)
+  prev +. a *. (curr -. prev)
+
+type t = (* abstract in mli *)
+  { mutable t0 : float;
+    mutable t : float;
+    totalToComplete : int64;
+    mutable completed : int64;
+    mutable curRate : float;
+    mutable avgRateS : float;
+    mutable avgRateDoubleSGauss : float;
+  }
+
+let gaussC = 2. *. (0.025 ** 2.)
+let avgPeriodS = 4.0
+let avgPeriodD = 3.5
+let calcPeriod = 0.25
+
+let init totalToTransfer =
+  let t0 = 0. in
+  { t0; t = t0; totalToComplete = Uutil.Filesize.toInt64 totalToTransfer;
+    completed = 0L;
+    curRate = Float.nan; avgRateS = Float.nan; avgRateDoubleSGauss = Float.nan;
+  }
+
+let calcAvgRate' sta totTime deltaCompleted deltaTime =
+  let curRate = (Int64.to_float deltaCompleted) /. deltaTime in
+  (* We want to ignore small fluctuations but react faster to large
+     changes (like switching from cache to disk or from disk to network
+     of from receiving to sending or with wildly variable network speed). *)
+  let avgRateS = movAvg curRate sta.avgRateS deltaTime
+    (Float.min_num totTime avgPeriodS) in
+  let cpr = (avgRateS -. sta.avgRateDoubleSGauss) /. sta.avgRateDoubleSGauss in
+  let c = 1. -. exp (-.(cpr ** 2.) /. gaussC) in
+  let avgRateDoubleSGauss = movAvg avgRateS sta.avgRateDoubleSGauss ~c deltaTime
+    (Float.min_num totTime avgPeriodD) in
+  sta.curRate <- curRate;
+  sta.avgRateS <- avgRateS;
+  sta.avgRateDoubleSGauss <- avgRateDoubleSGauss
+
+let update sta t1 totalCompleted =
+  let deltaTime = t1 -. sta.t in
+  let totalCompleted = Uutil.Filesize.toInt64 totalCompleted in
+  if sta.completed = 0L then begin
+    (* Skip the very first rate calculation because it will be skewed
+       due to (possibly significant) time losses during transport start. *)
+    sta.completed <- totalCompleted;
+    sta.t0 <- t1;
+    sta.t <- t1
+  end else if deltaTime >= calcPeriod then begin
+    let deltaCompleted = Int64.sub totalCompleted sta.completed in
+    sta.completed <- totalCompleted;
+    sta.t <- t1;
+    calcAvgRate' sta (t1 -. sta.t0) deltaCompleted deltaTime
+  end
+
+let curRate sta = sta.curRate
+let avgRate1 sta = sta.avgRateS
+let avgRate2 sta = sta.avgRateDoubleSGauss
+let eta sta ?(rate = sta.avgRateDoubleSGauss) default =
+  let rem = Int64.(to_float (sub sta.totalToComplete sta.completed)) in
+  let eta = calcETA rem rate in
+  if eta = "" then default else eta
+
+end
+
+(**********************************************************************
+                          Update propagation
+ **********************************************************************)
+
+(* (For context: the threads in question are all cooperating Lwt threads.
+   These are not OS threads or parallel running domains. There is no
+   preemption and only a single thread is executing at any time.)
+
+   Many (thousands) transport threads can run concurrently and
+   independently of each other. All threads run to completion as long as
+   there are no errors or only [Util.Transient] exceptions are raised.
+
+   The threads are _not_ guaranteed to run to completion when an exception
+   other than [Util.Transient] is raised in any of the threads. This means
+   that the threads may not even be able to complete their own cleanup
+   code and may leak resources. There must be separate resource cleanup code
+   that can be run after the threads have been stopped either forcefully or
+   by running to completion.
+
+   When an uncaught exception other than [Util.Transient] is raised in any
+   of the threads, the following happens:
+
+     - the thread raising the exception is aborted (all exception handlers
+       are run normally, so this thread is expected to run all its cleanup
+       code);
+
+     - any threads still waiting to be started are immediately cancelled
+       (they will not have run any meaningful code and don't require any
+       cleanup);
+
+     - any threads that have already run to completion are not impacted
+       in any way;
+
+     - any (sleeping) threads running concurrently with the raising thread
+       are stopped, which may have a different meaning depending on where
+       in the execution each thread was when it was stopped:
+
+         - some threads will not be able to continue at all (they are
+           never woken up);
+         - some threads may be woken up and may be able to continue
+           running for a short while but may get an error the next time
+           when accessing some resource (and may be able to run the error
+           handler);
+         - some threads may receive an exception and be able to continue
+           running for a short while.
+
+     - any exceptions raised in/by threads that are stopped, are ignored;
+
+     - the original exception from the first raising thread is reraised.
+
+   The code run in these thread must _not_ assume that:
+     - it will be run to completion;
+     - it can run some or all of its cleanup handlers;
+     - it will have had a chance to run a certain amount of its code;
+     - it will even know that it was stopped.
+   Depending on where and how a thread was stopped, it may be collected by
+   GC without any additional code ever being run in that thread.
+
+
+   There is a limit regulating how many threads can be run concurrently in
+   the [Transport] module. An attempt is made to not start threads that will
+   not be able to run due to this limit. This delayed starting is done for
+   two reasons. First, to make the cleanup in case of an uncaught exception
+   easier: if the thread was never run then there is nothing to clean up.
+   Second, even though the threads themselves are extremely lightweight,
+   they still consume some resources and this will add up when the number
+   of threads grows to hundreds of thousands and millions.
+
+   Not starting up all threads at once and allowing finished threads to
+   be collected by GC as soon as possible can potentially reduce the memory
+   requirement by gigabytes. (This is a reference to the old implementation
+   that started all threads in one go and kept them all around until all
+   were completed. This approach could result in running out of memory when
+   syncing large number of updates.) *)
+
+let transportStart () = Transport.logStart ()
+
+let transportFinish () = Transport.logFinish ()
+
+let transportItems items pRiThisRound makeAction =
+  let waiter = Lwt.wait () in
+  let outstanding = ref 0 in
+  let starting () = incr outstanding in
+  let completed () =
+    decr outstanding;
+    if !outstanding = 0 then begin
+      try Lwt.wakeup waiter () with Invalid_argument _ -> ()
+    end
+  in
+  let failed e =
+    try Lwt.wakeup_exn waiter e with Invalid_argument _ -> ()
+  in
+  let waitAllCompleted () =
+    if !outstanding = 0 then Lwt.return () else waiter
+  in
+
+  let im = Array.length items in
+  let idx = ref 0 in
+  let stopDispense () = idx := im in
+  let makeAction' i item =
+    Lwt.try_bind
+      (fun () -> starting (); makeAction i item)
+      (fun () -> completed (); Lwt.return ())
+      (fun ex -> stopDispense (); failed ex; Lwt.return ())
+  in
+  let dispenseDone =
+    let c = ref 0 in (* Make sure [completed] is never called more than once *)
+    fun () ->
+      if (incr c; !c = 1) then completed ();
+      None
+  in
+  let rec dispenseAction () =
+    let i = !idx in
+    if i < im then begin
+      let item = items.(i) in
+      incr idx;
+      if pRiThisRound item then
+        Some (fun () -> makeAction' i item)
+      else
+        dispenseAction ()
+    end else
+      dispenseDone ()
+  in
+
+  let doTransportFailCleanup () =
+    (* Don't start any new threads. *)
+    begin try stopDispense () with _ -> () end;
+    (* Stop all transfers. *)
+    begin try Abort.all () with _ -> () end;
+    (* Since we don't know what state the RPC protocol is in, we need
+       to close the remote connection to prevent hangs on select(2)
+       the next time [Lwt_unix.run] is called.
+
+       This will immediately trigger [on_close] handlers (which will
+       do some resource cleanup). *)
+    begin try
+      match Globals.rootsInCanonicalOrder () with
+      | [_; otherRoot] -> Remote.clientCloseRootConnection otherRoot
+      | _ -> assert false
+    with _ -> () end;
+    (* Threads that were still in the middle of execution are just
+       discarded and eventually collected by GC. Resources are cleaned
+       up and reclaimed by [Remote.at_conn_close] handlers.
+
+       Not all threads are stuck or purged and will continue the next
+       time [Lwt_unix.run] is called. Try to finish these threads now.
+       This must be done in a loop since each failing thread may raise
+       an uncaught exception which will end [Lwt_unix.run]. We don't
+       know how many threads can finish this way, so we don't know when
+       to stop looping. The limit of concurrent threads is used as an
+       approximation (which is probably much more than needed). *)
+    let rec loop_yield n () =
+      if n = 0 then Lwt.return () else Lwt_unix.yield () >>= loop_yield (n - 1)
+    in
+    for _ = 1 to Transport.maxThreads () do
+      try
+        Lwt_unix.run (loop_yield 10 ())
+      with _ -> ()
+    done
+  in
+
+  starting (); (* Count the dispense loop as one of the tasks to complete *)
+  try
+    Transport.run dispenseAction;
+    Lwt_unix.run (waitAllCompleted ())
+  with e -> begin
+    (* Cleanup procedure must never raise exceptions. Just in case,
+       don't shadow the original exception. *)
+    let origbt = Printexc.get_raw_backtrace () in
+    let () = doTransportFailCleanup () in
+    Printexc.raise_with_backtrace e origbt
+  end
+
 (**********************************************************************
                    Profile and command-line parsing
  **********************************************************************)
@@ -442,6 +771,8 @@ let debug = Trace.debug "startup"
 let architecture =
   Remote.registerRootCmd
     "architecture"
+    Umarshal.unit
+    Umarshal.(prod3 bool bool bool id id)
     (fun (_,()) -> return (Util.osType = `Win32, Osx.isMacOSX, Util.isCygwin))
 
 (* During startup the client determines the case sensitivity of each root.
@@ -475,6 +806,7 @@ let validateAndFixupPrefs () =
   Prefs.set Globals.someHostIsRunningWindows someHostIsRunningWindows;
   Prefs.set Globals.allHostsAreRunningWindows allHostsAreRunningWindows;
   if repeatWatcher () then Prefs.set Fswatch.useWatcher true;
+  Features.validateEnabled ();
   return ())
 
 (* ---- *)
@@ -492,29 +824,41 @@ let provideProfileKey filename k profile info =
       | Some(otherProfile,_) ->
           raise (Util.Fatal
             ("Error scanning profile "^
-                System.fspathToPrintString filename ^":\n"
+                filename ^":\n"
              ^ "shortcut key "^k^" is already bound to profile "
              ^ otherProfile))
     else
       raise (Util.Fatal
-        ("Error scanning profile "^ System.fspathToPrintString filename ^":\n"
+        ("Error scanning profile "^ filename ^":\n"
          ^ "Value of 'key' preference must be a single digit (0-9), "
          ^ "not " ^ k))
   with Failure _ -> raise (Util.Fatal
-    ("Error scanning profile "^ System.fspathToPrintString filename ^":\n"
+    ("Error scanning profile "^ filename ^":\n"
      ^ "Value of 'key' preference must be a single digit (0-9), "
      ^ "not " ^ k))
 
 let profilesAndRoots = ref []
 
 let scanProfiles () =
+  Os.createUnisonDir ();
   Array.iteri (fun i _ -> profileKeymap.(i) <- None) profileKeymap;
   profilesAndRoots :=
-    (Safelist.map
+    (Safelist.filterMap
        (fun f ->
           let f = Filename.chop_suffix f ".prf" in
           let filename = Prefs.profilePathname f in
-          let fileContents = Safelist.map (fun (_, _, n, v) -> (n, v)) (Prefs.readAFile f) in
+          let prefs =
+            try Some (Prefs.readAFile f) with
+            | Util.Fatal s -> begin
+                Util.warn ("Error when reading list of profiles.\n"
+                         ^ "Skipping file with error: "
+                         ^ filename
+                         ^ "\n\n" ^ s);
+                None end in
+          match prefs with
+          | None -> None
+          | Some prefs ->
+          let fileContents = Safelist.map (fun (_, n, v) -> (n, v)) prefs in
           let roots =
             Safelist.map snd
               (Safelist.filter (fun (n, _) -> n = "root") fileContents) in
@@ -530,7 +874,7 @@ let scanProfiles () =
              let k = Safelist.assoc "key" fileContents in
              provideProfileKey filename k f info
            with Not_found -> ());
-          (f, info))
+          Some (f, info))
        (Safelist.filter (fun name -> not (   Util.startswith name ".#"
                                           || Util.startswith name Os.tempFilePrefix))
           (Files.ls Util.unisonDir "*.prf")))
@@ -547,8 +891,7 @@ let initRoots displayWaitMessage termInteract =
      and install them in Globals. *)
   Lwt_unix.run (Globals.installRoots termInteract);
 
-  (* Expand any "wildcard" paths [with final component *] *)
-  Globals.expandWildcardPaths();
+  Files.processCommitLogs ();
 
   Update.storeRootsName ();
 
@@ -576,6 +919,9 @@ let initRoots displayWaitMessage termInteract =
          (Globals.rootsInCanonicalOrder());
        Printf.eprintf "\n");
 
+  (* Expand any "wildcard" paths [with final component *] *)
+  Globals.expandWildcardPaths ();
+
   Lwt_unix.run
     (validateAndFixupPrefs () >>=
      Globals.propagatePrefs);
@@ -586,39 +932,30 @@ let initRoots displayWaitMessage termInteract =
      on this side, that's why it's here. *)
   Stasher.initBackups ()
 
-let promptForRoots getFirstRoot getSecondRoot =
-  (* Ask the user for the roots *)
-  let r1 = match getFirstRoot() with None -> exit 0 | Some r -> r in
-  let r2 = match getSecondRoot() with None -> exit 0 | Some r -> r in
-  (* Remember them for this run, ordering them so that the first
-     will come out on the left in the UI *)
-  Globals.setRawRoots [r1; r2];
-  (* Save them in the current profile *)
-  ignore (Prefs.add "root" r1);
-  ignore (Prefs.add "root" r2)
-
 (* ---- *)
 
 let makeTempDir pattern =
   let path = Filename.temp_file pattern "" in
-  let fspath = System.fspathFromString path in
-  System.unlink fspath; (* Remove file created by [temp_file]... *)
-  System.mkdir fspath 0o755; (* ... and create a dir instead. *)
+  System.unlink path; (* Remove file created by [temp_file]... *)
+  System.mkdir path 0o755; (* ... and create a dir instead. *)
   path ^ Filename.dir_sep
 
-(* The first time we load preferences, we also read the command line
-   arguments; if we re-load prefs (because the user selected a new profile)
-   we ignore the command line *)
-let firstTime = ref(true)
+let initComplete = ref false
 
 (* Roots given on the command line *)
 let cmdLineRawRoots = ref []
 
+let clearClRoots () = cmdLineRawRoots := []
+
 (* BCP: WARNING: Some of the code from here is duplicated in uimacbridge...! *)
-let initPrefs ~profileName ~displayWaitMessage ~getFirstRoot ~getSecondRoot
-              ?(prepDebug = fun () -> ()) ~termInteract () =
-  (* Restore prefs to their default values, if necessary *)
-  if not !firstTime then Prefs.resetToDefaults();
+let initPrefs ~profileName ~promptForRoots ?(prepDebug = fun () -> ()) () =
+  initComplete := false;
+  (* Restore prefs to their default values *)
+  Prefs.resetToDefaults ();
+  (* Clear out any roots left from a previous profile. They can't remain
+     hanging around if [initPrefs] for the new profile receives an exception
+     before fully completing. *)
+  Globals.uninstallRoots ();
   Globals.setRawRoots !cmdLineRawRoots;
 
   (* Tell the preferences module the name of the profile *)
@@ -656,6 +993,9 @@ let initPrefs ~profileName ~displayWaitMessage ~getFirstRoot ~getSecondRoot
     Prefs.parseCmdLine usageMsg;
   end;
 
+  (* Turn on GC messages, if the '-debug gc' flag was provided *)
+  Gc.set {(Gc.get ()) with Gc.verbose = if Trace.enabled "gc" then 0x3F else 0};
+
   (* Install dummy roots and backup directory if we are running self-tests *)
   if Prefs.read runtests then begin
     let tmpdir = makeTempDir "unisontest" in
@@ -679,16 +1019,39 @@ let initPrefs ~profileName ~displayWaitMessage ~getFirstRoot ~getSecondRoot
   (* If no roots are given either on the command line or in the profile,
      ask the user *)
   if Globals.rawRoots() = [] then begin
-    promptForRoots getFirstRoot getSecondRoot;
+    (* Ask the user for the roots *)
+    match promptForRoots () with
+    | None -> raise (Util.Fatal "no roots given on command line or in profile")
+    | Some (r1, r2) ->
+        begin
+          (* Remember them for this run, ordering them so that the first
+             will come out on the left in the UI *)
+          Globals.setRawRoots [r1; r2];
+          (* Save them in the current profile *)
+          ignore (Prefs.add "root" r1);
+          ignore (Prefs.add "root" r2)
+        end
   end;
 
+  Trace.logonly "Roots:\n";
+  Globals.rawRoots () |> Safelist.iter
+    (fun s -> Trace.logonly "  "; Trace.logonly s; Trace.logonly "\n");
+  Trace.logonly "\n";
+
+  (* Parse the roots to validate them *)
+  let parsedRoots =
+    try Globals.parsedClRawRoots () with
+    | Invalid_argument s | Util.Fatal s | Prefs.IllegalValue s ->
+        raise (Util.Fatal ("There's a problem with one of the roots:\n" ^ s))
+  in
+
   (* Check to be sure that there is at most one remote root *)
   let numRemote =
     Safelist.fold_left
-      (fun n r -> match Clroot.parseRoot r with
+      (fun n (r : Clroot.clroot) -> match r with
         ConnectLocal _ -> n | ConnectByShell _ | ConnectBySocket _ -> n+1)
       0
-      (Globals.rawRoots ()) in
+      parsedRoots in
       if numRemote > 1 then
         raise(Util.Fatal "cannot synchronize more than one remote root");
 
@@ -700,16 +1063,15 @@ let initPrefs ~profileName ~displayWaitMessage ~getFirstRoot ~getSecondRoot
   (* If no paths were specified, then synchronize the whole replicas *)
   if Prefs.read Globals.paths = [] then Prefs.set Globals.paths [Path.empty];
 
-  initRoots displayWaitMessage termInteract;
-
-  firstTime := false
+  initComplete := true
 
-let refreshConnection ~displayWaitMessage ~termInteract =
-  assert (Safelist.length (Globals.rootsList ()) > 1);
+let connectRoots ?termInteract ~displayWaitMessage () =
+  let numRoots = Safelist.length (Globals.rawRoots ()) in
+  if !initComplete && numRoots > 1 then
   let numConn = ref 0 in
   Lwt_unix.run (Globals.allRootsIter
     (fun r -> if Remote.isRootConnected r then incr numConn; Lwt.return ()));
-  if !numConn < 2 then initRoots displayWaitMessage termInteract
+  if !numConn < numRoots then initRoots displayWaitMessage termInteract
 
 (**********************************************************************
                        Common startup sequence
@@ -717,10 +1079,13 @@ let refreshConnection ~displayWaitMessage ~termInteract =
 
 let anonymousArgs =
   Prefs.createStringList "rest"
+    ~category:(`Internal `Other)
     "*roots or profile name" ""
 
 let testServer =
   Prefs.createBool "testserver" false
+    ~category:(`Advanced `Remote)
+    ~cli_only:true
     "exit immediately after the connection to the server"
     ("Setting this flag on the command line causes Unison to attempt to "
      ^ "connect to the remote server and, if successful, print a message "
@@ -732,105 +1097,61 @@ let _ = Prefs.alias testServer "testServer"
 
 (* ---- *)
 
-let uiInit
-    ?(prepDebug = fun () -> ())
-    ~(reportError : string -> unit)
-    ~(tryAgainOrQuit : string -> bool)
-    ~(displayWaitMessage : unit -> unit)
-    ~(getProfile : unit -> string option)
-    ~(getFirstRoot : unit -> string option)
-    ~(getSecondRoot : unit -> string option)
-    ~(termInteract : (string -> string -> string) option)
-    () =
-
+let uiInitClRootsAndProfile ?(prepDebug = fun () -> ()) () =
   (* Make sure we have a directory for archives and profiles *)
   Os.createUnisonDir();
 
-  (* Extract any command line profile or roots *)
-  let clprofile = ref None in
+  let args = Prefs.scanCmdLine usageMsg in
   begin
-    let args = Prefs.scanCmdLine usageMsg in
-    begin
-      try if Util.StringMap.find "debug" args <> [] then prepDebug ()
-      with Not_found -> ()
-    end;
+    try if Util.StringMap.find "debug" args <> [] then prepDebug ()
+    with Not_found -> ()
+  end;
 
+  (* Extract any command line profile or roots *)
+  match begin
     try
       match Util.StringMap.find "rest" args with
-        [] -> ()
-      | [profile] -> clprofile := Some profile
-      | [root2;root1] -> cmdLineRawRoots := [root1;root2]
+      | [] -> Ok None
+      | [profile] -> Ok (Some profile)
+      | [root2;root1] -> Ok (cmdLineRawRoots := [root1;root2]; None)
       | [root2;root1;profile] ->
-          cmdLineRawRoots := [root1;root2];
-          clprofile := Some profile
+          Ok (cmdLineRawRoots := [root1;root2]; Some profile)
       | _ ->
-          (reportError(Printf.sprintf
-             "%s was invoked incorrectly (too many roots)" Uutil.myName);
-           exit 1)
-    with Not_found -> ()
-  end;
-
-  (* Print header for debugging output *)
-  debug (fun() ->
-    Printf.eprintf "%s, version %s\n\n" Uutil.myName Uutil.myVersion);
-  debug (fun() -> Util.msg "initializing UI");
-
-  debug (fun () ->
-    (match !clprofile with
-      None -> Util.msg "No profile given on command line"
-    | Some s -> Printf.eprintf "Profile '%s' given on command line" s);
-    (match !cmdLineRawRoots with
-      [] -> Util.msg "No roots given on command line"
-    | [root1;root2] ->
-        Printf.eprintf "Roots '%s' and '%s' given on command line"
-          root1 root2
-    | _ -> assert false));
-
-  let profileName =
-    begin match !clprofile with
-      None ->
-        let clroots_given = !cmdLineRawRoots <> [] in
-        let n =
-          if not(clroots_given) then begin
-            (* Ask the user to choose a profile or create a new one. *)
-            clprofile := getProfile();
-            match !clprofile with
-              None -> exit 0 (* None means the user wants to quit *)
-            | Some x -> x
-          end else begin
-            (* Roots given on command line.
-               The profile should be the default. *)
-            clprofile := Some "default";
-            "default"
-          end in
-        n
-    | Some n ->
-        let f = Prefs.profilePathname n in
-        if not(System.file_exists f)
-        then (reportError (Printf.sprintf "Profile %s does not exist"
-                             (System.fspathToPrintString f));
-              exit 1);
-        n
-    end in
-
-  (* Load the profile and command-line arguments *)
-  initPrefs
-    ~profileName ~displayWaitMessage ~getFirstRoot ~getSecondRoot
-    ~prepDebug ~termInteract ();
-
-  (* Turn on GC messages, if the '-debug gc' flag was provided *)
-  if Trace.enabled "gc" then Gc.set {(Gc.get ()) with Gc.verbose = 0x3F};
-
-  if Prefs.read testServer then exit 0;
-
-  (* BCPFIX: Should/can this be done earlier?? *)
-  Files.processCommitLogs();
-
-  (* Run unit tests if requested *)
-  if Prefs.read runtests then begin
-    (!testFunction)();
-    exit 0
-  end
+          Error (Printf.sprintf
+                   "%s was invoked incorrectly (too many roots)" Uutil.myName)
+    with Not_found -> Ok None
+  end with
+  | Error _ as e -> e
+  | Ok clprofile ->
+      debug (fun () ->
+        (* Print header for debugging output *)
+        Printf.eprintf "%s, version %s\n\n" Uutil.myName Uutil.myVersion;
+        Util.msg "initializing UI";
+
+        (match clprofile with
+         | None -> Util.msg "No profile given on command line"
+         | Some s -> Printf.eprintf "Profile '%s' given on command line" s);
+        (match !cmdLineRawRoots with
+         | [] -> Util.msg "No roots given on command line"
+         | [root1;root2] ->
+             Printf.eprintf "Roots '%s' and '%s' given on command line"
+               root1 root2
+         | _ -> assert false));
+
+      match clprofile with
+      | None when !cmdLineRawRoots = [] ->
+          (* Ask the user to choose a profile or create a new one. *)
+          Ok None
+      | None ->
+          (* Roots given on command line. The profile should be the default. *)
+          Ok (Some "default")
+      | Some n ->
+          let f = Prefs.profilePathname n in
+          if not (System.file_exists f)
+          then Error (Printf.sprintf
+                        "Profile '%s' does not exist (looking for file %s)"
+                        n f)
+          else Ok (Some n)
 
 (* Exit codes *)
 let perfectExit = 0   (* when everything's okay *)
diff --git a/src/uicommon.mli b/src/uicommon.mli
index 7c178db..de5affd 100644
--- a/src/uicommon.mli
+++ b/src/uicommon.mli
@@ -6,6 +6,8 @@ type interface =
    Text
  | Graphic
 
+val minterface : interface Umarshal.t
+
 (* The interface of a concrete UI implementation *)
 module type UI =
 sig
@@ -32,7 +34,8 @@ val contactingServerMsg : unit -> string
 val profileLabel : string Prefs.t
 
 (* User preference: Synchronize repeatedly *)
-val repeat : string Prefs.t
+val repeat : [ `NoRepeat | `Interval of int | `Watch
+  | `WatchAndInterval of int | `Invalid of string * exn ] Prefs.t
 
 (* User preference: Try failing paths N times *)
 val retry : int Prefs.t
@@ -42,6 +45,10 @@ val confirmmerge : bool Prefs.t
 
 val runTestsPrefName : string
 
+val runtests : bool Prefs.t
+
+val testServer : bool Prefs.t
+
 (* Format the information about current contents of a path in one replica (the second argument
    is used as a separator) *)
 val details2string : Common.reconItem -> string -> string
@@ -87,35 +94,27 @@ val usageMsg : string
 
 val shortUsageMsg : string
 
-val uiInit :
+val uiInitClRootsAndProfile :
     ?prepDebug:(unit -> unit) ->
-    reportError:(string -> unit) ->
-    tryAgainOrQuit:(string -> bool) ->
-    displayWaitMessage:(unit -> unit) ->
-    getProfile:(unit -> string option) ->
-    getFirstRoot:(unit -> string option) ->
-    getSecondRoot:(unit -> string option) ->
-    termInteract:(string -> string -> string) option ->
     unit ->
-    unit
+    (string option, string) result
 
 val initPrefs :
   profileName:string ->
-  displayWaitMessage:(unit->unit) ->
-  getFirstRoot:(unit->string option) ->
-  getSecondRoot:(unit->string option) ->
+  promptForRoots:(unit -> (string * string) option) ->
   ?prepDebug:(unit -> unit) ->
-  termInteract:(string -> string -> string) option ->
   unit ->
   unit
 
+val clearClRoots : unit -> unit
+
 (* Make sure remote connections (if any) corresponding to active roots
-   are still established and re-establish them if necessary.
-   [refreshConnection] is like [initPrefs] but without reloading the profile
-   and re-initializing the prefs. *)
-val refreshConnection :
+   are established and (re-)establish them if necessary.
+   [initPrefs] must be called before [connectRoots]. *)
+val connectRoots :
+  ?termInteract:(string -> Terminal.termInteract) ->
   displayWaitMessage:(unit -> unit) ->
-  termInteract:(string -> string -> string) option ->
+  unit ->
   unit
 
 val validateAndFixupPrefs : unit -> unit Lwt.t
@@ -139,3 +138,26 @@ val profileKeymap : (string * profileInfo) option array
 val profilesAndRoots : (string * profileInfo) list ref
 
 val scanProfiles : unit -> unit
+
+(* Update propagation *)
+val transportStart : unit -> unit
+val transportFinish : unit -> unit
+
+val transportItems : 'a array -> ('a -> bool) -> (int -> 'a -> unit Lwt.t) -> unit
+
+(* Statistics of update propagation *)
+module Stats : sig
+  type t
+  val init :
+       Uutil.Filesize.t (* Total size to complete 100% *)
+    -> t
+  val update :
+       t
+    -> float            (* Current absolute time *)
+    -> Uutil.Filesize.t (* Current completed size (not delta) *)
+    -> unit
+  val curRate : t -> float (* Current progress rate, very volatile *)
+  val avgRate1 : t -> float (* Moving average of the rate above, more stable *)
+  val avgRate2 : t -> float (* Double moving average, very stable *)
+  val eta : t -> ?rate:float -> string -> string (* Defaults to rate2 *)
+end
diff --git a/src/uigtk2.ml b/src/uigtk3.ml
similarity index 80%
rename from src/uigtk2.ml
rename to src/uigtk3.ml
index f8d73a2..c0092fe 100644
--- a/src/uigtk2.ml
+++ b/src/uigtk3.ml
@@ -1,4 +1,4 @@
-(* Unison file synchronizer: src/uigtk2.ml *)
+(* Unison file synchronizer: src/uigtk3.ml *)
 (* Copyright 1999-2020, Benjamin C. Pierce
 
     This program is free software: you can redistribute it and/or modify
@@ -60,13 +60,7 @@ must enter the host to connect to, a user name (if different from
 your user name on this machine), and the directory on the remote machine
 (relative to your home directory on that machine).
 
-2) To synchronize using RSH, there must be an RSH client installed on
-this machine and an RSH server installed on the remote machine.  You
-must enter the host to connect to, a user name (if different from
-your user name on this machine), and the directory on the remote machine
-(relative to your home directory on that machine).
-
-3) To synchronize using %s's socket protocol, there must be a %s
+2) To synchronize using %s's socket protocol, there must be a %s
 server running on the remote machine, listening to the port that you
 specify here.  (Use \"%s -socket xxx\" on the remote machine to
 start the %s server.)  You must enter the host, port, and the directory
@@ -92,23 +86,16 @@ let icon =
     (Gpointer.region_of_bytes Pixmaps.icon_data)
 *)
 let icon =
-  let p = GdkPixbuf.create ~width:48 ~height:48 ~has_alpha:true () in
-  let pxs = GdkPixbuf.get_pixels p in
-  (* This little hack is here to support compiling with lablgtk versions both
-     < 2.18.6 and >= 2.18.6 *)
-  String.iteri (fun i c -> Gpointer.set_byte pxs ~pos:i (Char.code c)) Pixmaps.icon_data;
-  p
+  lazy begin
+    let p = GdkPixbuf.create ~width:48 ~height:48 ~has_alpha:true () in
+    Gpointer.blit
+      ~src:(Gpointer.region_of_bytes (Bytes.of_string Pixmaps.icon_data))
+      ~dst:(GdkPixbuf.get_pixels p);
+    p
+  end
 
 let leftPtrWatch =
-  lazy
-     (let bitmap =
-        Gdk.Bitmap.create_from_data
-          ~width:32 ~height:32 Pixmaps.left_ptr_watch
-      in
-      let color =
-        Gdk.Color.alloc ~colormap:(Gdk.Color.get_system_colormap ()) `BLACK in
-      Gdk.Cursor.create_from_pixmap
-        (bitmap :> Gdk.pixmap) ~mask:bitmap ~fg:color ~bg:color ~x:2 ~y:2)
+  lazy (Gdk.Cursor.create `WATCH)
 
 let make_busy w =
   if Util.osType <> `Win32 then
@@ -145,10 +132,18 @@ let toplevelWindow () =
 let busy = ref false
 
 let getLock f =
+  let protect ~(finally : unit -> unit) f =
+    (* Very simple [protect] when we know that [finally] does not raise *)
+    (* FIXME: Switch to [Fun.protect] once OCaml 4.09 is the minimum? *)
+    try let () = f () in finally () with
+    | e ->
+        finally ();
+        raise e
+  in
   if !busy then
     Trace.status "Synchronizer is busy, please wait.."
   else begin
-    busy := true; f (); busy := false
+    busy := true; protect ~finally:(fun () -> busy := false) f
   end
 
 (**********************************************************************
@@ -266,21 +261,21 @@ let transcode s =
                        USEFUL LOW-LEVEL WIDGETS
  **********************************************************************)
 
-class scrolled_text ?editable ?shadow_type ?word_wrap
-    ~width ~height ?packing ?show
+class scrolled_text ?editable ?shadow_type ?(wrap_mode=`WORD) ?packing ?show
     () =
   let sw =
     GBin.scrolled_window ?packing ~show:false
-      ?shadow_type ~hpolicy:`NEVER ~vpolicy:`AUTOMATIC ()
+      ?shadow_type ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   in
-  let text = GText.view ?editable ~wrap_mode:`WORD ~packing:sw#add () in
+  let text = GText.view ?editable ~wrap_mode ~packing:sw#add () in
+  let () = text#set_left_margin 4
+  and () = text#set_right_margin 4 in
   object
     inherit GObj.widget_full sw#as_widget
     method text = text
     method insert s = text#buffer#set_text s;
     method show () = sw#misc#show ()
     initializer
-      text#misc#set_size_chars ~height ~width ();
       if show <> Some false then sw#misc#show ()
   end
 
@@ -306,8 +301,8 @@ let primaryText msg =
    chosen, false if the second button is chosen. *)
 let twoBox ?(kind=`DIALOG_WARNING) ~parent ~title ~astock ~bstock message =
   let t =
-    GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
-      ~allow_grow:false () in
+    GWindow.dialog ~parent ~border_width:6 ~modal:true
+      ~resizable:false () in
   t#vbox#set_spacing 12;
   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   ignore (GMisc.image ~stock:kind ~icon_size:`DIALOG
@@ -353,7 +348,7 @@ let warnBox ~parent title message =
     (* In batch mode, just pop up a window and go ahead *)
     let t =
       GWindow.dialog ~parent
-        ~border_width:6 ~modal:true ~no_separator:true ~allow_grow:false () in
+        ~border_width:6 ~modal:true ~resizable:false () in
     t#vbox#set_spacing 12;
     let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
     ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
@@ -406,27 +401,28 @@ class ['a] gMenuFactory
       (* Register this accel path *)
       GtkBase.Widget.set_accel_path item#as_widget accel_path accel_group;
       Gaux.may callback ~f:(fun callback -> item#connect#activate ~callback)
-    method add_item ?key ?modi ?callback ?submenu label =
+    method add_item ?key ?modi ?callback ?submenu ?bindname label =
       let item = GMenu.menu_item  ~use_mnemonic:true ~label () in
-      self#bind ?modi ?key ?callback label item;
+      self#bind ?modi ?key ?callback label ?name:bindname item;
       Gaux.may (submenu : GMenu.menu option) ~f:item#set_submenu;
       item
     method add_image_item ?(image : GObj.widget option)
         ?modi ?key ?callback ?stock ?name label =
+      (* GTK 3 does not provide image menu items (there is a way to
+         manually create a workaround but that does not work with
+         lablgtk. Let's create a regular menu item instead. *)
       let item =
-        GMenu.image_menu_item ~use_mnemonic:true ?image ~label ?stock () in
+        GMenu.menu_item ~use_mnemonic:true ~label () in
       match stock  with
       | None ->
-          self#bind ?modi ?key ?callback label ?name
-            (item : GMenu.image_menu_item :> GMenu.menu_item);
+          self#bind ?modi ?key ?callback label ?name item;
           item
       | Some s ->
           try
             let st = GtkStock.Item.lookup s in
             self#bind
               ?modi ?key:(if st.GtkStock.keyval=0 then key else None)
-              ?callback label ?name
-              (item : GMenu.image_menu_item :> GMenu.menu_item);
+              ?callback label ?name item;
             item
           with Not_found -> item
 
@@ -449,54 +445,83 @@ end
                          HIGHER-LEVEL WIDGETS
 ***********************************************************************)
 
+(* FIXME: This is a lowest-effort port of GTK2 pixmap-based code to GTK3.
+   It works but is probably needlessly inefficient(??). It should be
+   rewritten from scratch to match the new GTK(+Cairo) API and only draw
+   updated regions. *)
 class stats width height =
-  let pixmap = GDraw.pixmap ~width ~height () in
   let area =
-    pixmap#set_foreground `WHITE;
-    pixmap#rectangle ~filled:true ~x:0 ~y:0 ~width ~height ();
-    GMisc.pixmap pixmap ~width ~height ~xpad:4 ~ypad:8 ()
+    let d = GMisc.drawing_area () in
+    d#set_width_request width;
+    d#set_height_request height;
+    d#set_margin_left 4;
+    d#set_margin_right 4;
+    d#set_margin_top 8;
+    d#set_margin_bottom 8;
+    d#set_hexpand true;
+    d#set_vexpand true;
+    d
   in
   object (self)
     inherit GObj.widget_full area#as_widget
     val mutable maxim = ref 0.
     val mutable scale = ref 1.
     val mutable min_scale = 1.
-    val values = Array.make width 0.
+    val mutable values = Array.make width 0.
     val mutable active = false
+    val mutable width = float_of_int width
+    val mutable height = float_of_int height
+    initializer
+      ignore (area#misc#connect#size_allocate ~callback:self#resize);
+      ignore (area#misc#connect#draw ~callback:self#redraw)
+
+    method resize rect =
+      let oldw = truncate width in
+      let neww = min rect.Gtk.width 640 in
+      if neww > oldw then
+        values <- Array.append (Array.make (neww - oldw) 0.) (Array.sub values 0 oldw)
+      else if neww < oldw then begin
+        Array.blit values (oldw - neww) values 0 neww
+      end;
+      width <- float_of_int neww;
+      height <- float_of_int (min rect.Gtk.height 200);
+      area#misc#queue_draw ()
 
-    method redraw () =
+    method redraw cr =
       scale := min_scale;
       while !maxim > !scale do
         scale := !scale *. 1.5
       done;
-      pixmap#set_foreground `WHITE;
-      pixmap#rectangle ~filled:true ~x:0 ~y:0 ~width ~height ();
-      pixmap#set_foreground `BLACK;
-      for i = 0 to width - 1 do
-        self#rect i values.(max 0 (i - 1)) values.(i)
-      done
+      Cairo.set_source_rgb cr 1. 1. 1.;
+      Cairo.rectangle cr 0. 0. ~w:width ~h:height;
+      Cairo.fill cr;
+      for i = 0 to truncate width - 1 do
+        self#rect cr i values.(max 0 (i - 1)) values.(i)
+      done;
+      true
 
-    method activate a = active <- a; if a then self#redraw ()
+    method activate a = active <- a; if a then area#misc#queue_draw ()
 
-    method scale h = truncate ((float height) *. h /. !scale)
+    method scale h = height *. h /. !scale
 
-    method private rect i v' v =
+    method private rect cr i v' v =
       let h = self#scale v in
       let h' = self#scale v' in
       let h1 = min h' h in
       let h2 = max h' h in
-      pixmap#set_foreground `BLACK;
-      pixmap#rectangle
-        ~filled:true ~x:i ~y:(height - h1) ~width:1 ~height:h1 ();
-      for h = h1 + 1 to h2 do
-        let v = truncate (65535. *. (float (h - h1) /. float (h2 - h1))) in
-        let v = (v / 4096) * 4096 in (* Only use 16 gray levels *)
-        pixmap#set_foreground (`RGB (v, v, v));
-        pixmap#rectangle
-          ~filled:true ~x:i ~y:(height - h) ~width:1 ~height:1 ();
+      Cairo.set_source_rgb cr 0. 0. 0.;
+      Cairo.rectangle cr (float_of_int i) (height -. h1) ~w:1. ~h:h1;
+      Cairo.fill cr;
+      for h = (truncate h1) + 1 to (truncate h2) do
+        let v = ((float h -. h1) /. (h2 -. h1)) in
+        Cairo.set_source_rgb cr v v v;
+        Cairo.rectangle cr (float_of_int i) (height -. float h) ~w:1. ~h:1.;
+        Cairo.fill cr;
+        ()
       done
 
     method push v =
+      let width = truncate width in
       let need_max = values.(0) = !maxim in
       for i = 0 to width - 2 do
         values.(i) <- values.(i + 1)
@@ -508,18 +533,7 @@ class stats width height =
       end else
         maxim := max !maxim v;
       if active then begin
-        let need_resize =
-          !maxim > !scale || (!maxim > min_scale && !maxim < !scale /. 1.5) in
-        if need_resize then
-          self#redraw ()
-        else begin
-          pixmap#put_pixmap ~x:0 ~y:0 ~xsrc:1 (pixmap#pixmap);
-          pixmap#set_foreground `WHITE;
-          pixmap#rectangle
-            ~filled:true ~x:(width - 1) ~y:0 ~width:1 ~height ();
-          self#rect (width - 1) values.(width - 2) values.(width - 1)
-        end;
-        area#misc#draw None
+        area#misc#queue_draw ()
       end
   end
 
@@ -545,9 +559,20 @@ let rate2str v =
       " "
   end
 
+let mib = 1024. *. 1024.
+let kib2str v =
+  if v > 100_000_000. then
+    Format.sprintf "%.0f MiB" (v /. mib)
+  else if v > 1_000_000. then
+    Format.sprintf "%.1f MiB" (v /. mib)
+  else if v > 1024. then
+    Format.sprintf "%.1f KiB" (v /. 1024.)
+  else
+    Format.sprintf "%.0f B" v
+
 let statistics () =
   let title = "Statistics" in
-  let t = GWindow.dialog ~title () in
+  let t = GWindow.dialog ~title ~parent:(toplevelWindow ()) () in
   let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
   t_dismiss#grab_default ();
   let dismiss () = t#misc#hide () in
@@ -559,22 +584,28 @@ let statistics () =
   let reception = new stats 320 50 in
   t#vbox#pack ~expand:false ~padding:4 (reception :> GObj.widget);
 
-  let lst =
-    GList.clist
-      ~packing:(t#vbox#add)
-      ~titles_active:false
-      ~titles:[""; "Client"; "Server"; "Total"] ()
-  in
-  lst#set_column ~auto_resize:true 0;
-  lst#set_column ~auto_resize:true ~justification:`RIGHT 1;
-  lst#set_column ~auto_resize:true ~justification:`RIGHT 2;
-  lst#set_column ~auto_resize:true ~justification:`RIGHT 3;
-  ignore (lst#append ["Reception rate"]);
-  ignore (lst#append ["Data received"]);
-  ignore (lst#append ["File data written"]);
-  for r = 0 to 2 do
-    lst#set_row ~selectable:false r
-  done;
+  let cols = new GTree.column_list in
+  let c_1 = cols#add Gobject.Data.string in
+  let c_client = cols#add Gobject.Data.string in
+  let c_server = cols#add Gobject.Data.string in
+  let c_total = cols#add Gobject.Data.string in
+  let lst = GTree.list_store cols in
+  let l = GTree.view ~model:lst ~enable_search:false ~packing:(t#vbox#add) () in
+  l#selection#set_mode `NONE;
+  ignore (l#append_column (GTree.view_column ~title:""
+    ~renderer:(GTree.cell_renderer_text [], ["text", c_1]) ()));
+  ignore (l#append_column (GTree.view_column ~title:"Client"
+    ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_client]) ()));
+  ignore (l#append_column (GTree.view_column ~title:"Server"
+    ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_server]) ()));
+  ignore (l#append_column (GTree.view_column ~title:"Total"
+    ~renderer:(GTree.cell_renderer_text [`XALIGN 1.0], ["text", c_total]) ()));
+  let rate_row = lst#append () in
+  ignore (lst#set ~row:rate_row ~column:c_1 "Reception rate");
+  let receive_row = lst#append () in
+  ignore (lst#set ~row:receive_row ~column:c_1 "Data received");
+  let data_row = lst#append () in
+  ignore (lst#set ~row:data_row ~column:c_1 "File data written");
 
   ignore (t#event#connect#map ~callback:(fun _ ->
     emission#activate true;
@@ -597,19 +628,18 @@ let statistics () =
   let stopCounter = ref 0 in
 
   let updateTable () =
-    let kib2str v = Format.sprintf "%.0f B" v in
-    lst#set_cell ~text:(rate2str !receiveRate2) 0 1;
-    lst#set_cell ~text:(rate2str !emitRate2) 0 2;
-    lst#set_cell ~text:
-      (rate2str (!receiveRate2 +. !emitRate2)) 0 3;
-    lst#set_cell ~text:(kib2str !receivedBytes) 1 1;
-    lst#set_cell ~text:(kib2str !emittedBytes) 1 2;
-    lst#set_cell ~text:
-      (kib2str (!receivedBytes +. !emittedBytes)) 1 3;
-    lst#set_cell ~text:(kib2str !clientWritten) 2 1;
-    lst#set_cell ~text:(kib2str !serverWritten) 2 2;
-    lst#set_cell ~text:
-      (kib2str (!clientWritten +. !serverWritten)) 2 3
+    let row = rate_row in
+    lst#set ~row ~column:c_client (rate2str !receiveRate2);
+    lst#set ~row ~column:c_server (rate2str !emitRate2);
+    lst#set ~row ~column:c_total (rate2str (!receiveRate2 +. !emitRate2));
+    let row = receive_row in
+    lst#set ~row ~column:c_client (kib2str !receivedBytes);
+    lst#set ~row ~column:c_server (kib2str !emittedBytes);
+    lst#set ~row ~column:c_total (kib2str (!receivedBytes +. !emittedBytes));
+    let row = data_row in
+    lst#set ~row ~column:c_client (kib2str !clientWritten);
+    lst#set ~row ~column:c_server (kib2str !serverWritten);
+    lst#set ~row ~column:c_total (kib2str (!clientWritten +. !serverWritten))
   in
   let timeout _ =
     emitRate :=
@@ -648,31 +678,29 @@ let statistics () =
   let stopStats () = stopCounter := 10 in
   (t, startStats, stopStats)
 
-(****)
-
-(* Standard file dialog *)
-let file_dialog ~parent ~title ~callback ?filename () =
-  let sel = GWindow.file_selection ~parent ~title ~modal:true ?filename () in
-  ignore (sel#cancel_button#connect#clicked ~callback:sel#destroy);
-  ignore (sel#ok_button#connect#clicked ~callback:
-            (fun () ->
-               let name = sel#filename in
-               sel#destroy ();
-               callback name));
-  sel#show ();
-  ignore (sel#connect#destroy ~callback:GMain.Main.quit);
-  GMain.Main.main ()
-
 (* ------ *)
 
-let fatalError message =
+let gui_safe_eprintf fmt =
+  Printf.ksprintf (fun s ->
+    if System.has_stderr ~info:s then Printf.eprintf "%s%!" s) fmt
+
+let fatalError ?(quit=false) message =
   let () =
+    Trace.sendLogMsgsToStderr := false; (* We don't know if stderr is available *)
     try Trace.log (message ^ "\n")
     with Util.Fatal _ -> () in (* Can't allow fatal errors in fatal error handler *)
   let title = "Fatal error" in
+  let toplevelWindow =
+    try toplevelWindow ()
+    with Util.Fatal err ->
+      begin
+        gui_safe_eprintf "\n%s:\n%s\n\n%s\n\n" title err message;
+        exit 1
+      end
+  in
   let t =
-    GWindow.dialog ~parent:(toplevelWindow ())
-      ~border_width:6 ~modal:true ~no_separator:true ~allow_grow:false () in
+    GWindow.dialog ~parent:toplevelWindow
+      ~border_width:6 ~modal:true ~resizable:false () in
   t#vbox#set_spacing 12;
   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   ignore (GMisc.image ~stock:`DIALOG_ERROR ~icon_size:`DIALOG
@@ -683,19 +711,20 @@ let fatalError message =
                      escapeMarkup (transcode message))
             ~line_wrap:true ~selectable:true ~yalign:0. ~packing:v1#add ());
   t#add_button_stock `QUIT `QUIT;
-  t#set_default_response `QUIT;
+  if not quit then t#add_button_stock `CLOSE `CLOSE;
+  t#set_default_response (if quit then `QUIT else `CLOSE);
+  ignore (t#connect#response
+            ~callback:(function `QUIT -> exit 1 | _ -> ()));
   t#show(); ignore (t#run ()); t#destroy ();
-  exit 1
-
-(* ------ *)
+  if quit then exit 1
 
-let tryAgainOrQuit = fatalError
+let fatalErrorHandler = ref (fatalError ~quit:true)
 
 (* ------ *)
 
 let getFirstRoot () =
   let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
-      ~modal:true ~allow_grow:true () in
+      ~modal:true ~resizable:true () in
   t#misc#grab_focus ();
 
   let hb = GPack.hbox
@@ -709,20 +738,22 @@ let getFirstRoot () =
   ignore (GMisc.label ~text:"Dir:" ~packing:(f1#pack ~expand:false) ());
   let fileE = GEdit.entry ~packing:f1#add () in
   fileE#misc#grab_focus ();
-  let browseCommand() =
-    file_dialog ~parent:t ~title:"Select a local directory"
-      ~callback:fileE#set_text ~filename:fileE#text () in
-  let b = GButton.button ~label:"Browse"
-      ~packing:(f1#pack ~expand:false) () in
-  ignore (b#connect#clicked ~callback:browseCommand);
+  let b = GFile.chooser_button ~action:`SELECT_FOLDER
+    ~title:"Select a local directory"
+    ~packing:(f1#pack ~expand:false) () in
+  ignore (b#connect#selection_changed ~callback:(fun () ->
+            if not fileE#is_focus then
+              fileE#set_text (match b#filename with None -> "" | Some s -> s)));
+  ignore (fileE#connect#changed ~callback:(fun () ->
+            if fileE#is_focus then ignore (b#set_filename fileE#text)));
 
   let f3 = t#action_area in
   let result = ref None in
   let contCommand() =
-    result := Some(fileE#text);
+    result := Some (Util.trimWhitespace fileE#text);
     t#destroy () in
-  let quitButton = GButton.button ~stock:`QUIT ~packing:f3#add () in
-  ignore (quitButton#connect#clicked
+  let cancelButton = GButton.button ~stock:`CANCEL ~packing:f3#add () in
+  ignore (cancelButton#connect#clicked
             ~callback:(fun () -> result := None; t#destroy()));
   let contButton = GButton.button ~stock:`OK ~packing:f3#add () in
   ignore (contButton#connect#clicked ~callback:contCommand);
@@ -739,7 +770,7 @@ let getFirstRoot () =
 
 let getSecondRoot () =
   let t = GWindow.dialog ~parent:(toplevelWindow ()) ~title:"Root selection"
-      ~modal:true ~allow_grow:true () in
+      ~modal:true ~resizable:true () in
   t#misc#grab_focus ();
 
   let message = "Please enter the second directory you want to synchronize." in
@@ -762,12 +793,14 @@ let getSecondRoot () =
   ignore (GMisc.label ~text:"Directory:" ~packing:(f1#pack ~expand:false) ());
   let fileE = GEdit.entry ~packing:f1#add () in
   fileE#misc#grab_focus ();
-  let browseCommand() =
-    file_dialog ~parent:t ~title:"Select a local directory"
-      ~callback:fileE#set_text ~filename:fileE#text () in
-  let b = GButton.button ~label:"Browse"
-      ~packing:(f1#pack ~expand:false) () in
-  ignore (b#connect#clicked ~callback:browseCommand);
+  let b = GFile.chooser_button ~action:`SELECT_FOLDER
+    ~title:"Select a local directory"
+    ~packing:(f1#pack ~expand:false) () in
+  ignore (b#connect#selection_changed ~callback:(fun () ->
+            if not fileE#is_focus then
+              fileE#set_text (match b#filename with None -> "" | Some s -> s)));
+  ignore (fileE#connect#changed ~callback:(fun () ->
+            if fileE#is_focus then ignore (b#set_filename fileE#text)));
 
   let f0 = GPack.hbox ~spacing:4 ~packing:f#add () in
   let localB = GButton.radio_button ~packing:(f0#pack ~expand:false)
@@ -775,8 +808,6 @@ let getSecondRoot () =
   let sshB = GButton.radio_button ~group:localB#group
       ~packing:(f0#pack ~expand:false)
       ~label:"SSH" () in
-  let rshB = GButton.radio_button ~group:localB#group
-      ~packing:(f0#pack ~expand:false) ~label:"RSH" () in
   let socketB = GButton.radio_button ~group:sshB#group
       ~packing:(f0#pack ~expand:false) ~label:"Socket" () in
 
@@ -792,7 +823,7 @@ let getSecondRoot () =
             ~packing:(f2#pack ~expand:false) ());
   let portE = GEdit.entry ~packing:f2#add () in
 
-  let varLocalRemote = ref (`Local : [`Local|`SSH|`RSH|`SOCKET]) in
+  let varLocalRemote = ref (`Local : [`Local|`SSH|`SOCKET]) in
   let localState() =
     varLocalRemote := `Local;
     hostE#misc#set_sensitive false;
@@ -812,30 +843,29 @@ let getSecondRoot () =
     remoteState() in
   ignore (localB#connect#clicked ~callback:localState);
   ignore (sshB#connect#clicked ~callback:(fun () -> protoState(`SSH)));
-  ignore (rshB#connect#clicked ~callback:(fun () -> protoState(`RSH)));
   ignore (socketB#connect#clicked ~callback:(fun () -> protoState(`SOCKET)));
   localState();
   let getRoot() =
-    let file = fileE#text in
-    let user = userE#text in
-    let host = hostE#text in
-    let port = portE#text in
+    let file = Util.trimWhitespace fileE#text in
+    let user = Util.trimWhitespace userE#text in
+    let host = Util.trimWhitespace hostE#text in
+    let port = Util.trimWhitespace portE#text in
     match !varLocalRemote with
       `Local ->
         Clroot.clroot2string(Clroot.ConnectLocal(Some file))
-    | `SSH | `RSH ->
-        Clroot.clroot2string(
-        Clroot.ConnectByShell((if !varLocalRemote=`SSH then "ssh" else "rsh"),
+    | `SSH ->
+        Clroot.clroot2string(Clroot.fixHost(
+        Clroot.ConnectByShell("ssh",
                               host,
                               (if user="" then None else Some user),
                               (if port="" then None else Some port),
-                              Some file))
+                              Some file)))
     | `SOCKET ->
-        Clroot.clroot2string(
+        Clroot.clroot2string(Clroot.fixHost(
         (* FIX: report an error if the port entry is not well formed *)
         Clroot.ConnectBySocket(host,
                                portE#text,
-                               Some file)) in
+                               Some file))) in
   let contCommand() =
     try
       let root = getRoot() in
@@ -846,13 +876,14 @@ let getSecondRoot () =
         okBox ~parent:t ~title:"Error" ~typ:`ERROR ~message:"Please enter a port"
       else okBox ~parent:t ~title:"Error" ~typ:`ERROR
           ~message:"The port you specify must be an integer"
-    | _ ->
+    | Util.Transient s | Util.Fatal s | Invalid_argument s | Prefs.IllegalValue s ->
       okBox ~parent:t ~title:"Error" ~typ:`ERROR
-        ~message:"Something's wrong with the values you entered, try again" in
+        ~message:("Something's wrong with the values you entered, try again.\n" ^ s) in
   let f3 = t#action_area in
-  let quitButton =
-    GButton.button ~stock:`QUIT ~packing:f3#add () in
-  ignore (quitButton#connect#clicked ~callback:safeExit);
+  let cancelButton =
+    GButton.button ~stock:`CANCEL ~packing:f3#add () in
+  ignore (cancelButton#connect#clicked
+            ~callback:(fun () -> result := None; t#destroy ()));
   let contButton =
     GButton.button ~stock:`OK ~packing:f3#add () in
   ignore (contButton#connect#clicked ~callback:contCommand);
@@ -864,13 +895,29 @@ let getSecondRoot () =
   GMain.Main.main ();
   !result
 
+let promptForRoots () =
+  match getFirstRoot () with
+  | None -> None
+  | Some r1 ->
+      begin match getSecondRoot () with
+      | None -> None
+      | Some r2 -> Some (r1, r2)
+      end
+
 (* ------ *)
 
-let getPassword rootName msg =
+type 'a pwdDialog = {
+  labelAppend : string -> unit;
+  presentAndRun : unit -> unit;
+  closeInput : unit -> unit;
+}
+let passwordDialogs = ref []
+
+let createPasswordDialog passwordDialog rootName msg response =
   let t =
     GWindow.dialog ~parent:(toplevelWindow ())
       ~title:"Unison: SSH connection" ~position:`CENTER
-      ~no_separator:true ~modal:true ~allow_grow:false ~border_width:6 () in
+      ~modal:true ~resizable:false ~border_width:6 () in
   t#misc#grab_focus ();
 
   t#vbox#set_spacing 12;
@@ -883,9 +930,9 @@ let getPassword rootName msg =
   ignore (GMisc.image ~stock:`DIALOG_AUTHENTICATION ~icon_size:`DIALOG
             ~yalign:0. ~packing:h1#pack ());
   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
-  ignore(GMisc.label ~markup:(header ^ "\n\n" ^
+  let msgLbl = (GMisc.label ~markup:(header ^ "\n\n" ^
                               escapeMarkup (Unicode.protect msg))
-           ~selectable:true ~yalign:0. ~packing:v1#pack ());
+           ~selectable:true ~yalign:0. ~packing:v1#pack ()) in
 
   let passwordE = GEdit.entry ~packing:v1#pack ~visibility:false () in
   passwordE#misc#grab_focus ();
@@ -896,16 +943,50 @@ let getPassword rootName msg =
   ignore (passwordE#connect#activate ~callback:(fun _ -> t#response `OK));
 
   t#show();
-  let res = t#run () in
-  let pwd = passwordE#text in
-  t#destroy ();
-  gtk_sync true;
-  begin match res with
-    `DELETE_EVENT | `QUIT -> safeExit (); ""
-  | `OK                   -> pwd
-  end
 
-let termInteract = Some getPassword
+  let labelAppend msg =
+    msgLbl#set_label (msgLbl#label ^ escapeMarkup (Unicode.protect msg)) in
+  let presentAndRun () =
+    try t#present (); ignore (t#run ()) with Failure _ -> () in
+  let closeInput () =
+    passwordE#set_editable false; passwordE#set_visible false; passwordE#set_text "" in
+  passwordDialog := Some { labelAppend; presentAndRun; closeInput };
+
+  let callback res =
+    passwordDialog := None;
+    let pwd = passwordE#text in
+    let editable = passwordE#editable in
+    t#destroy ();
+    gtk_sync true;
+    match res with
+    | `DELETE_EVENT | `QUIT -> safeExit ()
+    | `OK -> if editable then response pwd
+  in
+  ignore (t#connect#response ~callback)
+
+let getPassword passwordDialog rootName msg response =
+  match !passwordDialog with
+  | Some { labelAppend; _ } -> labelAppend msg
+  | None -> createPasswordDialog passwordDialog rootName msg response
+
+let disablePassword passwordDialog () =
+  match !passwordDialog with
+  | Some { closeInput; _ } -> closeInput ()
+  | None -> ()
+
+let waitForPasswordWindowClosing () =
+  let present x =
+    match !x with
+    | Some { presentAndRun; _ } -> presentAndRun ()
+    | None -> ()
+  in
+  passwordDialogs :=
+    Safelist.filter (fun x -> present x; !x <> None) !passwordDialogs
+
+let termInteract rootName =
+  let d = ref None in
+  passwordDialogs := d :: !passwordDialogs;
+  { Terminal.userInput = getPassword d rootName; endInput = disablePassword d }
 
 (* ------ *)
 
@@ -1048,6 +1129,7 @@ let createProfile parent =
   assistant#set_modal true;
   assistant#set_title "Profile Creation";
 
+  let empty s = s = "" in
   let nonEmpty s = s <> "" in
 (*
   let integerRe =
@@ -1062,7 +1144,7 @@ let createProfile parent =
     GMisc.label
       ~xpad:12 ~ypad:12
       ~text:"Welcome to the Unison Profile Creation Assistant.\n\n\
-             Click \"Forward\" to begin."
+             Click \"Next\" to begin."
     () in
   ignore
     (assistant#append_page
@@ -1145,7 +1227,7 @@ let createProfile parent =
       GBin.alignment ~xscale:0. ~xalign:0.
         ~packing:(tbl#attach ~left:1 ~top:0) () in
     GEdit.combo_box_text
-      ~strings:["Local"; "Using SSH"; "Using RSH";
+      ~strings:["Local"; "Using SSH";
                 "Through a plain TCP connection"]
       ~active:0 ~packing:(al#add) ()
   in
@@ -1154,7 +1236,7 @@ let createProfile parent =
             ~packing:(tbl#attach ~left:0 ~top:0 ~expand:`NONE) ());
   let kind =
     GtkReact.text_combo kindCombo
-      >> fun i -> List.nth [`Local; `SSH; `RSH; `SOCKET] i
+      >> fun i -> List.nth [`Local; `SSH; `SOCKET] i
   in
   let isLocal = kind >> fun k -> k = `Local in
   let isSSH = kind >> fun k -> k = `SSH in
@@ -1173,9 +1255,6 @@ let createProfile parent =
           "This is the recommended way to synchronize \
            with a remote machine.  A\xc2\xa0remote instance of Unison is \
            automatically started via SSH."
-     | `RSH ->
-          "Synchronization with a remote machine by starting \
-           automatically a remote instance of Unison via RSH."
      | `SOCKET ->
           "Synchronization with a remote machine by connecting \
            to an instance of Unison already listening \
@@ -1200,9 +1279,6 @@ let createProfile parent =
      | `SSH ->
           "There must be an SSH client installed on this machine, \
            and Unison and an SSH server installed on the remote machine."
-     | `RSH ->
-          "There must be an RSH client installed on this machine, \
-           and Unison and an RSH server installed on the remote machine."
      | `SOCKET ->
           "There must be a Unison server running on the remote machine, \
            listening on the port that you specify here.  \
@@ -1219,8 +1295,6 @@ let createProfile parent =
        `Local  -> ""
      | `SSH    -> "Please enter the host to connect to and a user name, \
                    if different from your user name on this machine."
-     | `RSH    -> "Please enter the host to connect to and a user name, \
-                   if different from your user name on this machine."
      | `SOCKET -> "Please enter the host and port to connect to.");
   let tbl =
     let al = GBin.alignment ~packing:(vb#pack ~expand:false) () in
@@ -1288,10 +1362,14 @@ let createProfile parent =
   let connectImmediately =
     React.lift2 (&&) connectImmediately (isLocal >> not) in
 *)
+  let isNotUnixPath s = String.length s > 0 && s.[0] <> '{' in
+  let isTCPsocket = React.lift2 (&&) isSocket (host >> isNotUnixPath) in
   let pageComplete =
     React.lift2 (||) isLocal
       (React.lift2 (&&) (host >> nonEmpty)
-          (React.lift2 (||) (isSocket >> not) (port >> isInteger)))
+          (React.lift2 (||)
+              (React.lift2 (&&) isTCPsocket (port >> isInteger))
+              (React.lift2 (&&) (isTCPsocket >> not) (port >> empty))))
   in
   ignore
     (assistant#append_page
@@ -1493,17 +1571,52 @@ let createProfile parent =
        then
          assistant#set_current_page (p + 1)));
 
+  let conclusionOk = "You have now finished filling in the profile.\n\n\
+             Click \"Apply\" to create it."
+  and conclusionFail = "There was an error when preparing the profile.\n\n\
+             Click \"Back\" to review what you entered." in
   let conclusion =
     GMisc.label
       ~xpad:12 ~ypad:12
-      ~text:"You have now finished filling in the profile.\n\n\
-             Click \"Apply\" to create it."
+      ~text:conclusionOk
     () in
-  ignore
+  let conclusionp =
     (assistant#append_page
        ~title:"Done" ~complete:true
        ~page_type:`CONFIRM
-       conclusion#as_widget);
+       conclusion#as_widget) in
+
+  let makeRemoteRoot () =
+    let secondDir = Util.trimWhitespace (React.state secondDir) in
+    let host = Util.trimWhitespace (React.state host) in
+    let user = match React.state user with "" -> None | u -> Some (Util.trimWhitespace u) in
+    let secondRoot =
+      match React.state kind with
+        `Local  -> Clroot.ConnectLocal (Some secondDir)
+      | `SSH    -> Clroot.ConnectByShell
+                     ("ssh", host, user, None, Some secondDir)
+      | `SOCKET -> Clroot.ConnectBySocket
+                     (host, React.state port, Some secondDir)
+    in
+    try
+      let root = Clroot.clroot2string (Clroot.fixHost secondRoot) in
+      ignore (Clroot.parseRoot root);
+      Some root
+    with
+    | Util.Transient s | Util.Fatal s | Invalid_argument s | Prefs.IllegalValue s ->
+        begin
+          okBox ~parent ~title:"Error" ~typ:`ERROR
+            ~message:("There was a problem with the remote root "
+              ^ "data you entered.\n\n" ^ s);
+          None
+        end
+  in
+  ignore (assistant#connect#prepare ~callback:(fun () ->
+    if assistant#current_page = conclusionp then
+    let ok = (React.state kind = `Local) || (makeRemoteRoot () <> None) in
+    let () = setPageComplete conclusion ok in
+    if ok then conclusion#set_text conclusionOk
+    else conclusion#set_text conclusionFail));
 
   let profileName = ref None in
   let saveProfile () =
@@ -1512,24 +1625,20 @@ let createProfile parent =
       let ch =
         System.open_out_gen [Open_wronly; Open_creat; Open_excl] 0o600 filename
       in
+      let close_on_error f =
+        try f () with e -> close_out_noerr ch; raise e
+      in
+      close_on_error (fun () ->
       Printf.fprintf ch "# Unison preferences\n";
       let label = React.state label in
       if label <> "" then Printf.fprintf ch "label = %s\n" label;
       Printf.fprintf ch "root = %s\n" (React.state firstDir);
-      let secondDir = React.state secondDir in
-      let host = React.state host in
-      let user = match React.state user with "" -> None | u -> Some u in
       let secondRoot =
-        match React.state kind with
-          `Local  -> Clroot.ConnectLocal (Some secondDir)
-        | `SSH    -> Clroot.ConnectByShell
-                       ("ssh", host, user, None, Some secondDir)
-        | `RSH    -> Clroot.ConnectByShell
-                       ("rsh", host, user, None, Some secondDir)
-        | `SOCKET -> Clroot.ConnectBySocket
-                       (host, React.state port, Some secondDir)
+        match makeRemoteRoot () with
+        | None -> assert false (* We should never reach here due to validation above *)
+        | Some s -> s
       in
-      Printf.fprintf ch "root = %s\n" (Clroot.clroot2string secondRoot);
+      Printf.fprintf ch "root = %s\n" secondRoot;
       if React.state compress && React.state kind = `SSH then
         Printf.fprintf ch "sshargs = -C\n";
 (*
@@ -1539,7 +1648,7 @@ let createProfile parent =
         Printf.fprintf ch "unicode = true\n";
 *)
       if React.state fat then Printf.fprintf ch "fat = true\n";
-      close_out ch;
+      close_out ch);
       profileName := Some (React.state name)
     with Sys_error _ as e ->
       okBox ~parent:assistant ~typ:`ERROR ~title:"Could not save profile"
@@ -1579,7 +1688,7 @@ let defaultValue t =
 let editPreference parent nm ty vl =
   let t =
     GWindow.dialog ~parent ~border_width:12
-      ~no_separator:true ~title:"Edit the Preference"
+      ~title:"Edit the Preference"
       ~modal:true () in
   let vb = t#vbox in
   vb#set_spacing 6;
@@ -1602,7 +1711,7 @@ let editPreference parent nm ty vl =
             ~packing:(tbl#attach ~left:0 ~top:2 ~expand:`NONE) ());
   ignore (GMisc.label ~text:(Unicode.protect nm) ~xalign:0. ~selectable:true ()
             ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X));
-  let (doc, _, _) = Prefs.documentation nm in
+  let (doc, _) = Prefs.documentation nm in
   ignore (GMisc.label ~text:doc ~xalign:0. ~selectable:true ()
             ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X));
   ignore (GMisc.label ~text:(nameOfType ty) ~xalign:0. ~selectable:true ()
@@ -1870,6 +1979,10 @@ let documentPreference ~compact ~packing =
     in
     GText.view ~editable:false ~packing:sw#add ~wrap_mode:`WORD ()
   in
+  let () = longDescr#set_left_margin 4
+  and () = longDescr#set_right_margin 4
+  and () = longDescr#set_top_margin 1
+  and () = longDescr#set_bottom_margin 2 in
   let (>>>) x f = f x in
   let newlineRe = Str.regexp "\n *" in
   let styleRe = Str.regexp "{\\\\\\([a-z]+\\) \\([^{}]*\\)}" in
@@ -1914,14 +2027,14 @@ let documentPreference ~compact ~packing =
      ("tt", create [`FONT_DESC (Lazy.force fontMonospace)])]
   in
   fun nm ->
-    let (short, long, _) =
+    let (short, long) =
       match nm with
         Some nm ->
           tbl#misc#set_sensitive true;
           Prefs.documentation nm
       | _ ->
           tbl#misc#set_sensitive false;
-          ("", "", false)
+          ("", "")
     in
     shortDescr#set_text (String.capitalize_ascii short);
     insertMarkup tags longDescr (formatDoc long)
@@ -1930,13 +2043,14 @@ let documentPreference ~compact ~packing =
 let addPreference parent =
   let t =
     GWindow.dialog ~parent ~border_width:12
-      ~no_separator:true ~title:"Add a Preference"
+      ~title:"Add a Preference"
       ~modal:true () in
+  t#set_default_height 575;
   let vb = t#vbox in
 (*  vb#set_spacing 18;*)
   let paned = GPack.paned `VERTICAL ~packing:vb#add () in
 
-  let lvb = GPack.vbox ~spacing:6 ~packing:paned#pack1 () in
+  let lvb = GPack.vbox ~spacing:6 ~packing:(paned#pack1 ~resize:true) () in
   let preferenceLabel =
     GMisc.label
       ~text:"_Preferences:" ~use_underline:true
@@ -1944,8 +2058,8 @@ let addPreference parent =
   in
   let cols = new GTree.column_list in
   let c_name = cols#add Gobject.Data.string in
-  let basic_store = GTree.list_store cols in
-  let full_store = GTree.list_store cols in
+  let c_font = cols#add Gobject.Data.string in
+  let store = GTree.tree_store cols in
   let lst =
     let sw =
       GBin.scrolled_window ~packing:(lvb#pack ~expand:true)
@@ -1953,54 +2067,98 @@ let addPreference parent =
         ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC () in
     GTree.view ~headers_visible:false ~packing:sw#add () in
   preferenceLabel#set_mnemonic_widget (Some (lst :> GObj.widget));
-  ignore (lst#append_column
-    (GTree.view_column
-       ~renderer:(GTree.cell_renderer_text [], ["text", c_name]) ()));
-  let hiddenPrefs =
-    ["auto"; "doc"; "silent"; "terse"; "testserver"; "version"] in
+
+  let cell_r = GTree.cell_renderer_text [] in
+  let view_col = (GTree.view_column ~renderer:(cell_r, ["text", c_name]) ()) in
+  view_col#add_attribute cell_r "font" c_font;
+  ignore (lst#append_column view_col);
+  (*let hiddenPrefs =
+    ["auto"; "silent"; "terse"] in*)
   let shownPrefs =
     ["label"; "key"] in
-  let insert (store : #GTree.list_store) all =
+
+  let createGroup n =
+    let row = store#append () in
+    store#set ~row ~column:c_name n;
+    store#set ~row ~column:c_font "bold";
+    row
+  in
+  let createTopic parent n =
+    let row = store#append ~parent () in
+    store#set ~row ~column:c_name n;
+    store#set ~row ~column:c_font "italic";
+    row
+  in
+  let createTopics parent g =
+    Safelist.map (fun t ->
+      let topic = g t in
+      (topic, (createTopic parent (Prefs.topic_title topic))))
+  in
+
+  let topicsInOrder = [ `Sync; `Syncprocess; `Syncprocess_CLI; `CLI; `GUI; `Remote; `Archive ] in
+
+  let basic = createGroup "1 — Basic preferences" in
+  let l = createTopics basic (fun t -> `Basic t) (`General :: topicsInOrder) in
+
+  let adv = createGroup "2 — Advanced preferences" in
+  let l = l @ createTopics adv (fun t -> `Advanced t) (topicsInOrder @ [`General]) in
+
+  let l = (`Expert, createGroup "3 — Expert preferences") :: l in
+
+  let parents = l in
+  let purgeParents () =
+    Safelist.iter (fun (_, row) ->
+        if not (store#iter_has_child row) then begin
+          let parent = store#iter_parent row in
+          ignore (store#remove row);
+          match parent with
+          | None -> ()
+          | Some parent -> if not (store#iter_has_child parent) then
+                             ignore (store#remove parent)
+        end
+      ) parents
+  in
+  let categoryParent nm =
+    match Prefs.category nm with
+    | None -> None
+    | Some _ when List.mem nm shownPrefs -> Some basic
+    | Some cat -> begin
+        try Some (Safelist.assoc cat parents) with
+        | Not_found -> None
+      end
+  in
+  let isParent r = store#iter_has_child r in
+
+  let () =
     List.iter
       (fun nm ->
-         if
-           all || List.mem nm shownPrefs ||
-           (let (_, _, basic) = Prefs.documentation nm in basic &&
-            not (List.mem nm hiddenPrefs))
-         then begin
-           let row = store#append () in
-           store#set ~row ~column:c_name nm
-         end)
-      (Prefs.list ())
+         let row =
+           match categoryParent nm with
+           | None -> store#append ()
+           | Some parent -> store#append ~parent ()
+         in
+         store#set ~row ~column:c_name nm
+      )
+      (Prefs.list false);
   in
-  insert basic_store false;
-  insert full_store true;
+  purgeParents ();
 
-  let showAll =
-    GtkReact.toggle_button
-      (GButton.check_button ~label:"_Show all preferences"
-         ~use_mnemonic:true ~active:false ~packing:(lvb#pack ~expand:false) ())
-  in
-  showAll >|
-    (fun b ->
-       lst#set_model
-         (Some (if b then full_store else basic_store :> GTree.model)));
+  lst#set_model (Some store#coerce);
 
+  let getSelectedPref rf =
+    let row = rf#iter in
+    if isParent row then
+      None
+    else
+      Some (store#get ~row ~column:c_name)
+  in
   let selection = GtkReact.tree_view_selection lst in
   let updateDoc = documentPreference ~compact:true ~packing:paned#pack2 in
-  selection >|
-    (fun l ->
-       let nm =
-         match l with
-           [rf] ->
-             let row = rf#iter in
-             let store =
-               if React.state showAll then full_store else basic_store in
-             Some (store#get ~row ~column:c_name)
-         | _ ->
-             None
-       in
-       updateDoc nm);
+  let prefSelection = selection >> (function
+    | [rf] -> getSelectedPref rf
+    | _ -> None)
+  in
+  prefSelection >| updateDoc;
 
   let cancelCommand () = t#destroy () in
   let cancelButton =
@@ -2012,7 +2170,7 @@ let addPreference parent =
   let addButton =
     GButton.button ~stock:`ADD ~packing:t#action_area#add () in
   ignore (addButton#connect#clicked ~callback:addCommand);
-  GtkReact.set_sensitive addButton (selection >> fun l -> l <> []);
+  GtkReact.set_sensitive addButton (prefSelection >> fun nm -> nm <> None);
   ignore (lst#connect#row_activated ~callback:(fun _ _ -> addCommand ()));
   addButton#grab_default ();
 
@@ -2021,18 +2179,15 @@ let addPreference parent =
   GMain.Main.main ();
   if not !ok then None else
     match React.state selection with
-      [rf] ->
-        let row = rf#iter in
-        let store =
-          if React.state showAll then full_store else basic_store in
-        Some (store#get ~row ~column:c_name)
+    | [rf] ->
+        getSelectedPref rf
     | _ ->
         None
 
 let editProfile parent name =
   let t =
     GWindow.dialog ~parent ~border_width:12
-      ~no_separator:true ~title:(Format.sprintf "%s - Profile Editor" name)
+      ~title:(Format.sprintf "%s - Profile Editor" name)
       ~modal:true () in
   let vb = t#vbox in
 (*  t#vbox#set_spacing 18;*)
@@ -2201,7 +2356,7 @@ let editProfile parent name =
   in
   let (>>>) x f = f x in
   Prefs.readAFile name
-  >>> List.map (fun (_, _, nm, v) -> Prefs.canonicalName nm, v)
+  >>> List.map (fun (_, nm, v) -> Prefs.canonicalName nm, v)
   >>> List.stable_sort (fun (nm, _) (nm', _) -> compare nm nm')
   >>> group
   >>> List.iter
@@ -2225,6 +2380,10 @@ let editProfile parent name =
           System.open_out_gen [Open_wronly; Open_creat; Open_trunc] 0o600
             filename
         in
+        let close_on_error f =
+          try f () with e -> close_out_noerr ch; raise e
+        in
+        close_on_error (fun () ->
   (*XXX Should trim whitespaces and check for '\n' at some point  *)
         Printf.fprintf ch "# Unison preferences\n";
         lst_store#foreach
@@ -2232,7 +2391,7 @@ let editProfile parent name =
              let (nm, _, vl) = lst_store#get ~row ~column:c_ml in
              List.iter (fun v -> Printf.fprintf ch "%s = %s\n" nm v) vl;
              false);
-        close_out ch;
+        close_out ch);
         setModified false
       with Sys_error _ as e ->
         okBox ~parent:t ~typ:`ERROR ~title:"Could not save profile"
@@ -2277,15 +2436,24 @@ TODO:
 
 (* ------ *)
 
+let documentationFn = ref (fun ~parent _ -> ())
+
 let getProfile quit =
   let ok = ref false in
+  let parent = toplevelWindow () in
+  (* Make sure that a potentially open password window from a (failed) previous
+     session is not hidden underneath this window. *)
+  waitForPasswordWindowClosing ();
 
   (* Build the dialog *)
   let t =
-    GWindow.dialog ~parent:(toplevelWindow ()) ~border_width:12
-      ~no_separator:true ~title:"Profile Selection"
-      ~modal:true () in
+    GWindow.dialog ~parent ~border_width:12
+      ~title:"Profile Selection"
+      ~modal:false () in
   t#set_default_width 550;
+  (* Simulate modal dialog (allowing to open other windows, such as help) *)
+  parent#set_sensitive false;
+  ignore (t#connect#destroy ~callback:(fun () -> parent#set_sensitive true));
 
   let cancelCommand _ = t#destroy () in
   let cancelButton =
@@ -2351,10 +2519,10 @@ let getProfile quit =
             ~packing:(tbl#attach ~left:0 ~top:1 ~expand:`NONE) ());
   let root1 =
     GMisc.label ~packing:(tbl#attach ~left:1 ~top:0 ~expand:`X)
-      ~xalign:0. ~selectable:true () in
+      ~xalign:0. ~selectable:true ~ellipsize:`MIDDLE () in
   let root2 =
     GMisc.label ~packing:(tbl#attach ~left:1 ~top:1 ~expand:`X)
-      ~xalign:0. ~selectable:true () in
+      ~xalign:0. ~selectable:true ~ellipsize:`MIDDLE () in
 
   let fillLst default =
     Uicommon.scanProfiles();
@@ -2397,9 +2565,10 @@ let getProfile quit =
            tbl#misc#set_sensitive false);
   GtkReact.set_sensitive okButton hasSel;
 
+  let box = GPack.vbox ~packing:(hb#pack ~expand:false) () in
   let vb =
     GPack.button_box
-      `VERTICAL ~layout:`START ~spacing:6 ~packing:(hb#pack ~expand:false) ()
+      `VERTICAL ~layout:`START ~spacing:6 ~packing:(box#pack ~expand:false) ()
   in
   let addButton =
     GButton.button ~stock:`ADD ~packing:(vb#pack ~expand:false) () in
@@ -2439,6 +2608,13 @@ let getProfile quit =
   GtkReact.set_sensitive deleteButton hasSel;
   List.iter (fun b -> b#set_xalign 0.) [addButton; editButton; deleteButton];
 
+  ignore (GPack.vbox ~packing:(box#pack ~expand:true) ());
+  let helpButton =
+    GButton.button ~stock:`HELP ~packing:(box#pack ~expand:false) () in
+  helpButton#set_xalign 0.;
+  ignore (helpButton#connect#clicked
+     ~callback:(fun () -> !documentationFn ~parent:t ""));
+
   ignore (lst#connect#row_activated ~callback:(fun _ _ -> okCommand ()));
   fillLst None;
   lst#misc#grab_focus ();
@@ -2451,9 +2627,14 @@ let getProfile quit =
 
 (* ------ *)
 
-let documentation sect =
+let get_size_chars obj ?desc ?lang ~height ~width () =
+  let metrics = obj#misc#pango_context#get_metrics ?desc ?lang () in
+  (width * GPango.to_pixels metrics#approx_digit_width,
+   height * GPango.to_pixels (metrics#ascent+metrics#descent))
+
+let documentation ~parent sect =
   let title = "Documentation" in
-  let t = GWindow.dialog ~title () in
+  let t = GWindow.dialog ~title ~parent () in
   let t_dismiss =
     GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
   t_dismiss#grab_default ();
@@ -2461,48 +2642,53 @@ let documentation sect =
   ignore (t_dismiss#connect#clicked ~callback:dismiss);
   ignore (t#event#connect#delete ~callback:(fun _ -> dismiss (); true));
 
-  let (name, docstr) = Safelist.assoc sect Strings.docs in
-  let hb = GPack.hbox ~packing:(t#vbox#pack ~expand:false ~padding:2) () in
-  let optionmenu =
-    GMenu.option_menu ~packing:(hb#pack ~expand:true ~fill:false) () in
+  let nb = GPack.notebook ~show_tabs:true ~tab_pos:`LEFT ~border_width:5
+    ~packing:(t#vbox#pack ~expand:true) () in
 
-  let t_text =
-    new scrolled_text ~editable:false
-      ~width:80 ~height:20 ~packing:t#vbox#add ()
+  let sect_idx = ref 0 in
+  let add_nb_page label active w =
+    let i = nb#append_page ~tab_label:label#coerce w in
+    if active then sect_idx := i
   in
-  t_text#insert docstr;
 
-  let sect_idx = ref 0 in
-  let idx = ref 0 in
-  let menu = GMenu.menu () in
+  let lw = ref 1 in
   let addDocSection (shortname, (name, docstr)) =
-    if shortname <> "" && name <> "" then begin
-      if shortname = sect then sect_idx := !idx;
-      incr idx;
-      let item = GMenu.menu_item ~label:name ~packing:menu#append () in
-      ignore
-        (item#connect#activate ~callback:(fun () -> t_text#insert docstr))
-    end
+    if shortname = "" || name = "" then () else
+    let () = lw := max !lw (String.length name) in
+    let label = GMisc.label ~markup:("<b>" ^ name ^ "</b>")
+                  ~xalign:1. ~justify:`LEFT ~ellipsize:`NONE () in
+    let box = GBin.frame ~border_width:8
+                ~packing:(add_nb_page label (shortname = sect)) () in
+    let text = new scrolled_text ~editable:false ~wrap_mode:`NONE
+                 ~packing:box#add () in
+    text#insert docstr
   in
   Safelist.iter addDocSection Strings.docs;
-  optionmenu#set_menu menu;
-  optionmenu#set_history !sect_idx;
+
+  nb#goto_page !sect_idx;
+
+  let (width, height) = get_size_chars t ~width:(80 + !lw) ~height:25 () in
+  t#set_default_size ~width ~height;
 
   t#show ()
+let () = documentationFn := documentation
 
 (* ------ *)
 
 let messageBox ~title ?(action = fun t -> t#destroy) message =
   let utitle = transcode title in
-  let t = GWindow.dialog ~title:utitle ~position:`CENTER () in
+  let t = GWindow.dialog ~title:utitle ~parent:(toplevelWindow ())
+            ~position:`CENTER () in
   let t_dismiss = GButton.button ~stock:`CLOSE ~packing:t#action_area#add () in
   t_dismiss#grab_default ();
   ignore (t_dismiss#connect#clicked ~callback:(action t));
   let t_text =
-    new scrolled_text ~editable:false
-      ~width:80 ~height:20 ~packing:t#vbox#add ()
+    new scrolled_text ~editable:false ~wrap_mode:`NONE
+      ~packing:(t#vbox#pack ~expand:true) ()
   in
   t_text#insert message;
+  let (width, height) = get_size_chars t_text ~width:82 ~height:20 () in
+  t#set_default_size ~width ~height;
   ignore (t#event#connect#delete ~callback:(fun _ -> action t (); true));
   t#show ()
 
@@ -2513,8 +2699,8 @@ let messageBox ~title ?(action = fun t -> t#destroy) message =
 let twoBoxAdvanced
       ~parent ~title ~message ~longtext ~advLabel ~astock ~bstock =
   let t =
-    GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
-      ~allow_grow:false () in
+    GWindow.dialog ~parent ~border_width:6 ~modal:true
+      ~resizable:false () in
   t#vbox#set_spacing 12;
   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   ignore (GMisc.image ~stock:`DIALOG_QUESTION ~icon_size:`DIALOG
@@ -2546,8 +2732,8 @@ let twoBoxAdvanced
 
 let summaryBox ~parent ~title ~message ~f =
   let t =
-    GWindow.dialog ~parent ~border_width:6 ~modal:true ~no_separator:true
-      ~allow_grow:false ~focus_on_map:false () in
+    GWindow.dialog ~parent ~border_width:6 ~modal:true
+      ~resizable:true ~focus_on_map:true () in
   t#vbox#set_spacing 12;
   let h1 = GPack.hbox ~border_width:6 ~spacing:12 ~packing:t#vbox#pack () in
   ignore (GMisc.image ~stock:`DIALOG_INFO ~icon_size:`DIALOG
@@ -2555,12 +2741,15 @@ let summaryBox ~parent ~title ~message ~f =
   let v1 = GPack.vbox ~spacing:12 ~packing:h1#pack () in
   ignore (GMisc.label
             ~markup:(primaryText title ^ "\n\n" ^ escapeMarkup message)
-            ~selectable:true ~xalign:0. ~yalign:0. ~packing:v1#add ());
-  let exp = GBin.expander ~spacing:12 ~label:"Show details" ~packing:v1#add () in
+            ~selectable:true ~xalign:0. ~yalign:0. ~packing:(v1#pack ~expand:false) ());
+  let exp = GBin.expander ~spacing:12 ~label:"Show details"
+              ~packing:(v1#pack ~expand:true) () in
   let t_text =
-    new scrolled_text ~editable:false ~shadow_type:`IN
-      ~width:60 ~height:10 ~packing:exp#add ()
-  in
+    new scrolled_text ~editable:false ~shadow_type:`IN ~packing:exp#add () in
+  t_text#set_expand true;
+  let (width, height) = get_size_chars t_text ~width:60 ~height:10 () in
+  t_text#set_width_request width;
+  t_text#set_height_request height;
   f (t_text#text);
   t#add_button_stock `OK `OK;
   t#set_default_response `OK;
@@ -2578,6 +2767,15 @@ let displayWaitMessage () =
   make_busy (toplevelWindow ());
   Trace.status (Uicommon.contactingServerMsg ())
 
+let prepDebug () =
+  if Sys.os_type = "Win32" then
+    (* As a side-effect, this allocates a console if the process doesn't
+       have one already. This call is here only for the side-effect,
+       because debugging output is produced on stderr and the GUI will
+       crash if there is no stderr. *)
+    try ignore (System.terminalStateFunctions ())
+    with Unix.Unix_error _ -> ()
+
 (* ------ *)
 
 type status = NoStatus | Done | Failed
@@ -2590,7 +2788,7 @@ let createToplevelWindow () =
   setToplevelWindow toplevelWindow;
   (* There is already a default icon under Windows, and transparent
      icons are not supported by all version of Windows *)
-  if Util.osType <> `Win32 then toplevelWindow#set_icon (Some icon);
+  if Util.osType <> `Win32 then toplevelWindow#set_icon (Some (Lazy.force icon));
   let toplevelVBox = GPack.vbox ~packing:toplevelWindow#add () in
 
   (*******************************************************************
@@ -2606,6 +2804,7 @@ let createToplevelWindow () =
   let grDiff = ref [] in
   let grGo = ref [] in
   let grRescan = ref [] in
+  let grStop = ref [] in
   let grDetail = ref [] in
   let grAdd gr w = gr := w#misc::!gr in
   let grSet gr st = Safelist.iter (fun x -> x#set_sensitive st) !gr in
@@ -2614,6 +2813,7 @@ let createToplevelWindow () =
     grSet grDiff false;
     grSet grGo false;
     grSet grRescan false;
+    grSet grStop false;
     grSet grDetail false
   in
 
@@ -2655,9 +2855,11 @@ let createToplevelWindow () =
       | "default", _  -> label
       | _             -> Format.sprintf "%s (%s)" p label
     in
+    let roots = String.concat " ↔ " (Globals.rawRoots ()) in
+    let roots = if roots = "" then "" else "   |   " ^ roots in
     toplevelWindow#set_title
       (if s = "" then myNameCapitalized else
-       Format.sprintf "%s [%s]" myNameCapitalized s);
+       Format.sprintf "%s [%s]%s" myNameCapitalized s roots);
     let s = if s="" then "No profile" else "Profile: " ^ s in
     profileLabel#set_text (transcode s)
   in
@@ -2676,13 +2878,12 @@ let createToplevelWindow () =
     Action bar
    *********************************************************************)
   let actionBar =
-    let hb = GBin.handle_box ~packing:(toplevelVBox#pack ~expand:false) () in
     GButton.toolbar ~style:`BOTH
       (* 2003-0519 (stse): how to set space size in gtk 2.0? *)
       (* Answer from Jacques Garrigue: this can only be done in
          the user's.gtkrc, not programmatically *)
-      ~orientation:`HORIZONTAL ~tooltips:true (* ~space_size:10 *)
-      ~packing:(hb#add) () in
+      ~orientation:`HORIZONTAL (* ~space_size:10 *)
+      ~packing:(toplevelVBox#pack ~expand:false) () in
 
   (*********************************************************************
     Create the main window
@@ -2691,13 +2892,6 @@ let createToplevelWindow () =
       GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:true)
         ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   in
-  let sizeMainWindow () =
-    let ctx = mainWindowSW#misc#pango_context in
-    let metrics = ctx#get_metrics () in
-    let h = GPango.to_pixels (metrics#ascent+metrics#descent) in
-    toplevelWindow#set_default_height
-      ((h + 3) * (Prefs.read Uicommon.mainWindowHeight + 1) + 200)
-  in
   let cols = new GTree.column_list in
   let c_replica1 = cols#add Gobject.Data.string in
   let c_action   = cols#add Gobject.Data.gobject in
@@ -2748,7 +2942,6 @@ let createToplevelWindow () =
          " " ^ Unicode.protect (String.sub s 15 12) ^ " "; "  Status  ";
          " Path" |];
   in
-  sizeMainWindow ();
 
   (* See above for comment about tree path index and [theState] array index
      equivalence. *)
@@ -2823,17 +3016,20 @@ let createToplevelWindow () =
 
   let detailsWindowSW =
     GBin.scrolled_window ~packing:(toplevelVBox#pack ~expand:false)
-        ~shadow_type:`IN ~hpolicy:`NEVER ~vpolicy:`AUTOMATIC ()
+        ~shadow_type:`IN ~hpolicy:`AUTOMATIC ~vpolicy:`AUTOMATIC ()
   in
   let detailsWindow =
     GText.view ~editable:false ~packing:detailsWindowSW#add ()
   in
+  let (width, height) = get_size_chars detailsWindow ~height:4 ~width:112 () in
+  let () = detailsWindowSW#set_height_request height in
+  (* width is set in [sizeMainWindow] *)
+
   let detailsWindowPath = detailsWindow#buffer#create_tag [] in
   let detailsWindowInfo =
     detailsWindow#buffer#create_tag [`FONT_DESC (Lazy.force fontMonospace)] in
   let detailsWindowError =
     detailsWindow#buffer#create_tag [`WRAP_MODE `WORD] in
-  detailsWindow#misc#set_size_chars ~height:3 ~width:112 ();
   detailsWindow#misc#set_can_focus false;
 
   let updateButtons () =
@@ -2932,12 +3128,16 @@ let createToplevelWindow () =
   let progressBar =
     GRange.progress_bar ~packing:(statusHBox#pack ~expand:false) () in
 
-  progressBar#misc#set_size_chars ~height:1 ~width:28 ();
+  progressBar#misc#modify_font detailsWindow#misc#pango_context#font_description;
+  let (w, _) = get_size_chars progressBar ~width:28 ~height:1 () in
+  progressBar#set_width_request w;
+  progressBar#set_show_text true;
   progressBar#set_pulse_step 0.02;
   let progressBarPulse = ref false in
 
   let statusWindow =
     GMisc.statusbar ~packing:(statusHBox#pack ~expand:true) () in
+  statusWindow#set_margin 0;
   let statusContext = statusWindow#new_context ~name:"status" in
   ignore (statusContext#push "");
 
@@ -2956,6 +3156,65 @@ let createToplevelWindow () =
   Trace.statusFormatter := formatStatus;
   Trace.sendLogMsgsToStderr := false;
 
+
+  (* Window is created before initPrefs but we don't want the size to
+     jump around after window has been shown (which is inevitable when
+     height is specified in a profile). Scan the command line to check
+     for height preference. *)
+  begin try
+    let prefName = List.hd (Prefs.name Uicommon.mainWindowHeight) in
+    let clHeight = List.hd (Util.StringMap.find prefName (Prefs.scanCmdLine "")) in
+    Prefs.set Uicommon.mainWindowHeight (int_of_string clHeight)
+  with Not_found | Invalid_argument _ | Util.Fatal _ -> () end;
+
+  let calcWinSize () =
+    (* (Poor) approximation of row height. It is impossible to get real
+       GTK TreeView row height (and it depends on theme). *)
+    let row_height = (List.hd mainWindow#all_children)#misc#allocation.height in
+    let height =
+      if row_height < 2 then   (* Oops, sizes clearly not allocated yet *)
+        let metrics = mainWindowSW#misc#pango_context#get_metrics () in
+        let h = GPango.to_pixels (metrics#ascent + metrics#descent) in
+        (h + 8) * (8 + (Prefs.read Uicommon.mainWindowHeight)) (* rought default *)
+      else
+          topHBox#misc#allocation.height
+        + actionBar#misc#allocation.height
+        + 2 * mainWindow#border_width  (* top and bottom *)
+        + row_height  (* column headers *)
+        + (row_height - 3) * (Prefs.read Uicommon.mainWindowHeight)
+        + detailsWindowSW#misc#allocation.height
+        + statusHBox#misc#allocation.height
+    in
+    let height = min height (Gdk.Screen.height ~screen:toplevelWindow#screen ()) in
+    let width =
+      let metrics = mainWindowSW#misc#pango_context#get_metrics () in
+      let w = GPango.to_pixels metrics#approx_digit_width in
+      max (w * 112) 860
+    in
+    let width = min width (Gdk.Screen.width ~screen:toplevelWindow#screen ()) in
+    (height, width)
+  in
+
+  let prevHeightPref = ref 0 in
+
+  let sizeMainWindow () =
+    (* Only update height if the preference changed, otherwise risk undoing
+       user's manual height adjustments. Also assume no change if the
+       preference is at the default value. *)
+    let prefHeight = Prefs.read Uicommon.mainWindowHeight in
+    if !prevHeightPref <> prefHeight &&
+        (!prevHeightPref = 0 ||
+          prefHeight <> Prefs.readDefault Uicommon.mainWindowHeight) then begin
+      let (height, _) = calcWinSize ()
+      and width = toplevelWindow#misc#allocation.width in
+      toplevelWindow#resize ~height ~width
+    end;
+    prevHeightPref := prefHeight
+  in
+  let (height, width) = calcWinSize () in
+  toplevelWindow#set_default_size ~height ~width;
+  ignore (toplevelWindow#misc#connect#show ~callback:sizeMainWindow);
+
   (*********************************************************************
     Functions used to print in the main window
    *********************************************************************)
@@ -3099,10 +3358,11 @@ let createToplevelWindow () =
       displayRow row i r1 r2 action status path;
     done;
     mainWindow#set_model (Some mainWindowModel#coerce);
-    match savedCurrent with
+    begin match savedCurrent with
     | []  -> selectSomethingIfPossible ()
     | [x] -> select x true
-    | _   -> Safelist.iter (fun p -> mainWindow#selection#select_path p) savedCurrent;
+    | _   -> Safelist.iter (fun p -> mainWindow#selection#select_path p) savedCurrent
+    end;
 
     progressBar#set_text ""; progressBar#set_fraction 0.;
     updateDetails ();  (* Do we need this line? *)
@@ -3139,11 +3399,9 @@ let createToplevelWindow () =
   let totalBytesToTransfer = ref Uutil.Filesize.zero in
   let totalBytesTransferred = ref Uutil.Filesize.zero in
 
-  let t0 = ref 0. in
   let t1 = ref 0. in
   let lastFrac = ref 0. in
-  let oldWritten = ref 0. in
-  let writeRate = ref 0. in
+  let sta = ref (Uicommon.Stats.init (Uutil.Filesize.zero)) in
   let displayGlobalProgress v =
     if v = 0. || abs_float (v -. !lastFrac) > 1. then begin
       lastFrac := v;
@@ -3153,21 +3411,15 @@ let createToplevelWindow () =
       progressBar#set_text " "
     else begin
       let t = Unix.gettimeofday () in
+      Uicommon.Stats.update !sta t !totalBytesTransferred;
       let delta = t -. !t1 in
       if delta >= 0.5 then begin
         t1 := t;
         let remTime =
           if v >= 100. then "00:00 remaining" else
-          let t = truncate ((!t1 -. !t0) *. (100. -. v) /. v +. 0.5) in
-          Format.sprintf "%02d:%02d remaining" (t / 60) (t mod 60)
+          (Uicommon.Stats.eta !sta "--:--") ^ " remaining"
         in
-        let written = !clientWritten +. !serverWritten in
-        let b = 0.64 ** delta in
-        writeRate :=
-          b *. !writeRate +.
-          (1. -. b) *. (written -. !oldWritten) /. delta;
-        oldWritten := written;
-        let rate = !writeRate (*!emitRate2 +. !receiveRate2*) in
+        let rate = Uicommon.Stats.avgRate1 !sta in
         let txt =
           if rate > 99. then
             Format.sprintf "%s  (%s)" remTime (rate2str rate)
@@ -3198,8 +3450,8 @@ let createToplevelWindow () =
     root2IsLocal := fst root2 = Local;
     totalBytesToTransfer := b;
     totalBytesTransferred := Uutil.Filesize.zero;
-    t0 := Unix.gettimeofday (); t1 := !t0;
-    writeRate := 0.; oldWritten := !clientWritten +. !serverWritten;
+    t1 := Unix.gettimeofday ();
+    sta := Uicommon.Stats.init !totalBytesToTransfer;
     displayGlobalProgress 0.
   in
 
@@ -3276,7 +3528,10 @@ let createToplevelWindow () =
   let clearMainWindow () =
     grDisactivateAll ();
     make_busy toplevelWindow;
+    mainWindow#set_model None;
     mainWindowModel#clear ();
+    mainWindow#set_model (Some mainWindowModel#coerce);
+    theState := [||];
     detailsWindow#buffer#set_text ""
   in
 
@@ -3288,7 +3543,7 @@ let createToplevelWindow () =
     let findUpdates () =
       let t = Trace.startTimer "Checking for updates" in
       Trace.status "Looking for changes";
-      let updates = Update.findUpdates ~wantWatcher:() !unsynchronizedPaths in
+      let updates = Update.findUpdates ~wantWatcher:true !unsynchronizedPaths in
       Trace.showTimer t;
       updates in
     let reconcile updates =
@@ -3337,13 +3592,14 @@ let createToplevelWindow () =
     Help menu
    *********************************************************************)
   let addDocSection (shortname, (name, docstr)) =
+    let parent = toplevelWindow in
     if shortname = "about" then
       ignore (helpMenu#add_image_item
-                ~stock:`ABOUT ~callback:(fun () -> documentation shortname)
+                ~stock:`ABOUT ~callback:(fun () -> documentation ~parent shortname)
                 name)
     else if shortname <> "" && name <> "" then
       ignore (helpMenu#add_item
-                ~callback:(fun () -> documentation shortname)
+                ~callback:(fun () -> documentation ~parent shortname)
                 name) in
   Safelist.iter addDocSection Strings.docs;
 
@@ -3418,7 +3674,8 @@ let createToplevelWindow () =
       make_busy toplevelWindow;
 
       Trace.status "Propagating changes";
-      Transport.logStart ();
+      Uicommon.transportStart ();
+      grSet grStop true;
       let totalLength =
         Array.fold_left
           (fun l si ->
@@ -3432,78 +3689,54 @@ let createToplevelWindow () =
           Uutil.Filesize.zero !theState in
       initGlobalProgress totalLength;
       let t = Trace.startTimer "Propagating changes" in
-      let im = Array.length !theState in
-      let rec loop i actions pRiThisRound =
-        if i < im then begin
-          let theSI = !theState.(i) in
-          let textDetailed = ref None in
-          let action =
-            match theSI.whatHappened with
-              None ->
-                if not (pRiThisRound theSI.ri) then
-                  return ()
-                else
-                  catch (fun () ->
-                           Transport.transportItem
-                             theSI.ri (Uutil.File.ofLine i)
-                             (fun title text ->
-                               textDetailed := (Some text);
-                               if Prefs.read Uicommon.confirmmerge then
-                                 twoBoxAdvanced
-                                   ~parent:toplevelWindow
-                                   ~title:title
-                                   ~message:("Do you want to commit the changes to"
-                                             ^ " the replicas ?")
-                                   ~longtext:text
-                                   ~advLabel:"View details..."
-                                   ~astock:`YES
-                                   ~bstock:`NO
-                               else
-                                 true)
-                           >>= (fun () ->
-                             return Util.Succeeded))
-                         (fun e ->
-                           match e with
-                             Util.Transient s ->
-                               return (Util.Failed s)
-                           | _ ->
-                               fail e)
-                    >>= (fun res ->
-                      let rem =
-                        Uutil.Filesize.sub
-                          theSI.bytesToTransfer theSI.bytesTransferred
-                      in
-                      if rem <> Uutil.Filesize.zero then
-                        showProgress (Uutil.File.ofLine i) rem "done";
-                      theSI.whatHappened <- Some (res, !textDetailed);
-                  fastRedisplay i;
-(* JV (7/09): It does not seem that useful to me to scroll the display
-   to make the first unfinished item visible.  The scrolling is way
-   too fast, and it makes it impossible to browse the list. *)
-(*
-                  sync_action :=
-                    Some
-                      (fun () ->
-                         makeFirstUnfinishedVisible pRiThisRound;
-                         sync_action := None);
-*)
-                  gtk_sync false;
-                  return ())
-            | Some _ ->
-                return () (* Already processed this one (e.g. merged it) *)
-          in
-          loop (i + 1) (action :: actions) pRiThisRound
-        end else
-          actions
+      let uiWrapper i theSI =
+        match theSI.whatHappened with
+          None ->
+            let textDetailed = ref None in
+            catch (fun () ->
+                     Transport.transportItem
+                       theSI.ri (Uutil.File.ofLine i)
+                       (fun title text ->
+                         textDetailed := (Some text);
+                         if Prefs.read Uicommon.confirmmerge then
+                           twoBoxAdvanced
+                             ~parent:toplevelWindow
+                             ~title:title
+                             ~message:("Do you want to commit the changes to"
+                                       ^ " the replicas ?")
+                             ~longtext:text
+                             ~advLabel:"View details..."
+                             ~astock:`YES
+                             ~bstock:`NO
+                         else
+                           true)
+                     >>= (fun () ->
+                       return Util.Succeeded))
+                   (fun e ->
+                     match e with
+                       Util.Transient s ->
+                         return (Util.Failed s)
+                     | _ ->
+                         fail e)
+              >>= (fun res ->
+                let rem =
+                  Uutil.Filesize.sub
+                    theSI.bytesToTransfer theSI.bytesTransferred
+                in
+                if rem <> Uutil.Filesize.zero then
+                  showProgress (Uutil.File.ofLine i) rem "done";
+                theSI.whatHappened <- Some (res, !textDetailed);
+            fastRedisplay i;
+            gtk_sync false;
+            return ())
+        | Some _ ->
+            return () (* Already processed this one (e.g. merged it) *)
       in
       startStats ();
-      Lwt_unix.run
-        (let actions = loop 0 [] (fun ri -> not (Common.isDeletion ri)) in
-         Lwt_util.join actions);
-      Lwt_unix.run
-        (let actions = loop 0 [] Common.isDeletion in
-         Lwt_util.join actions);
-      Transport.logFinish ();
+      Uicommon.transportItems !theState (fun {ri; _} -> not (Common.isDeletion ri)) uiWrapper;
+      Uicommon.transportItems !theState (fun {ri; _} -> Common.isDeletion ri) uiWrapper;
+      Uicommon.transportFinish ();
+      grSet grStop false;
       Trace.showTimer t;
       commitUpdates ();
       stopStats ();
@@ -3567,19 +3800,29 @@ let createToplevelWindow () =
         if skippedCount = 0 then [] else
         [Printf.sprintf "%d skipped" skippedCount]
       in
+      let nostartCount =
+        if not (Abort.isAll ()) then 0 else
+          Array.fold_left
+            (fun c si -> if si.whatHappened = None then c + 1 else c)
+            0 !theState
+      in
+      let nostart =
+        if nostartCount = 0 then [] else
+        [Printf.sprintf "%d not started" nostartCount]
+      in
       unsynchronizedPaths :=
         Some (Safelist.map (fun (si, _, _) -> si.ri.path1)
                 (failureList @ partialList @ skippedList),
               []);
       Trace.status
         (Printf.sprintf "Synchronization complete         %s"
-           (String.concat ", " (failures @ partials @ skipped)));
+           (String.concat ", " (failures @ partials @ skipped @ nostart)));
       displayGlobalProgress 0.;
 
       grSet grRescan true;
       make_interactive toplevelWindow;
 
-      let totalCount = failureCount + partialCount + skippedCount in
+      let totalCount = failureCount + partialCount + skippedCount + nostartCount in
       if totalCount > 0 then begin
         let format n item sing plur =
           match n with
@@ -3590,10 +3833,12 @@ let createToplevelWindow () =
         let infos =
           format failureCount "failure" "" "s" @
           format partialCount "partially transferred director" "y" "ies" @
-          format skippedCount "skipped item" "" "s"
+          format skippedCount "skipped item" "" "s" @
+          format nostartCount "not started item" "" "s"
         in
         let message =
-          (if failureCount = 0 then "The synchronization was successful.\n\n"
+          (if failureCount = 0 && nostartCount = 0 then
+             "The synchronization was successful.\n\n"
            else "") ^
           "The replicas are not fully synchronized.\n" ^
           (if totalCount < 2 then "There was" else "There were") ^
@@ -3608,7 +3853,7 @@ let createToplevelWindow () =
           ~title:"Synchronization summary" ~message ~f:
           (fun t ->
              let bullet = "\xe2\x80\xa2 " in
-             let layout = t#misc#pango_context#create_layout in
+             let layout = Pango.Layout.create t#misc#pango_context#as_context in
              Pango.Layout.set_text layout bullet;
              let (n, _) = Pango.Layout.get_pixel_size layout in
              let path =
@@ -3674,35 +3919,39 @@ let createToplevelWindow () =
   let questionAction _ = doAction (fun _ diff -> diff.direction <- Conflict "") in
   let mergeAction    _ = doAction (fun _ diff -> diff.direction <- Merge) in
 
+  let insert_button (toolbar : #GButton.toolbar) ~stock ~text ~tooltip ~callback () =
+    let b = GButton.tool_button ~stock ~label:text ~packing:toolbar#insert () in
+    ignore (b#connect#clicked ~callback);
+    b#misc#set_tooltip_text tooltip;
+    b
+  in
+
 (*  actionBar#insert_space ();*)
   grAdd grAction
-    (actionBar#insert_button
-(*       ~icon:((GMisc.pixmap rightArrowBlack ())#coerce)*)
-       ~icon:((GMisc.image ~stock:`GO_FORWARD ())#coerce)
+    (insert_button actionBar
+       ~stock:`GO_FORWARD
        ~text:"Left to Right"
        ~tooltip:"Propagate selected items\n\
                  from the left replica to the right one"
        ~callback:rightAction ());
 (*  actionBar#insert_space ();*)
   grAdd grAction
-    (actionBar#insert_button ~text:"Skip"
-       ~icon:((GMisc.image ~stock:`NO ())#coerce)
+    (insert_button actionBar ~text:"Skip"
+       ~stock:`NO
        ~tooltip:"Skip selected items"
        ~callback:questionAction ());
 (*  actionBar#insert_space ();*)
   grAdd grAction
-    (actionBar#insert_button
-(*       ~icon:((GMisc.pixmap leftArrowBlack ())#coerce)*)
-       ~icon:((GMisc.image ~stock:`GO_BACK ())#coerce)
+    (insert_button actionBar
+       ~stock:`GO_BACK
        ~text:"Right to Left"
        ~tooltip:"Propagate selected items\n\
                  from the right replica to the left one"
        ~callback:leftAction ());
 (*  actionBar#insert_space ();*)
   grAdd grAction
-    (actionBar#insert_button
-(*       ~icon:((GMisc.pixmap mergeLogoBlack())#coerce)*)
-       ~icon:((GMisc.image ~stock:`ADD ())#coerce)
+    (insert_button actionBar
+       ~stock:`ADD
        ~text:"Merge"
        ~tooltip:"Merge selected files"
        ~callback:mergeAction ());
@@ -3735,9 +3984,9 @@ let createToplevelWindow () =
     | None ->
         () in
 
-  actionBar#insert_space ();
-  grAdd grDiff (actionBar#insert_button ~text:"Diff"
-                  ~icon:((GMisc.image ~stock:`DIALOG_INFO ())#coerce)
+  actionBar#insert (GButton.separator_tool_item ());
+  grAdd grDiff (insert_button actionBar ~text:"Diff"
+                  ~stock:`DIALOG_INFO
                   ~tooltip:"Compare the two files at each replica"
                   ~callback:diffCmd ());
 
@@ -3745,8 +3994,8 @@ let createToplevelWindow () =
     Detail button
    *********************************************************************)
 (*  actionBar#insert_space ();*)
-  grAdd grDetail (actionBar#insert_button ~text:"Details"
-                    ~icon:((GMisc.image ~stock:`INFO ())#coerce)
+  grAdd grDetail (insert_button actionBar ~text:"Details"
+                    ~stock:`INFO
                     ~tooltip:"Show detailed information about\n\
                               an item, when available"
                     ~callback:showDetCommand ());
@@ -3764,46 +4013,37 @@ let createToplevelWindow () =
   (*********************************************************************
     go button
    *********************************************************************)
-  actionBar#insert_space ();
+  actionBar#insert (GButton.separator_tool_item ());
   grAdd grGo
-    (actionBar#insert_button ~text:"Go"
+    (insert_button actionBar ~text:"Go"
        (* tooltip:"Go with displayed actions" *)
-       ~icon:((GMisc.image ~stock:`EXECUTE ())#coerce)
+       ~stock:`EXECUTE
        ~tooltip:"Perform the synchronization"
        ~callback:(fun () ->
                     getLock synchronize) ());
 
-  (* Does not quite work: too slow, and Files.copy must be modifed to
-     support an interruption without error. *)
-  (*
-  ignore (actionBar#insert_button ~text:"Stop"
-            ~icon:((GMisc.image ~stock:`STOP ())#coerce)
-            ~tooltip:"Exit Unison"
-            ~callback:Abort.all ());
-  *)
+  grAdd grStop
+    (insert_button actionBar ~text:"Stop"
+       ~stock:`STOP
+       ~tooltip:"Stop update propagation"
+       ~callback:Abort.all ());
 
   (*********************************************************************
     Rescan button
    *********************************************************************)
+  let profileInitSuccess = ref false in
   let updateFromProfile = ref (fun () -> ()) in
 
-  let prepDebug () =
-    if Sys.os_type = "Win32" then
-      (* As a side-effect, this allocates a console if the process doesn't
-         have one already. This call is here only for the side-effect,
-         because debugging output is produced on stderr and the GUI will
-         crash if there is no stderr. *)
-      try ignore (System.terminalStateFunctions ())
-      with Unix.Unix_error _ -> ()
-  in
-
   let loadProfile p reload =
     debug (fun()-> Util.msg "Loading profile %s..." p);
     Trace.status "Loading profile";
     unsynchronizedPaths := None;
-    Uicommon.initPrefs ~profileName:p
+    profileInitSuccess := false;
+    Uicommon.initPrefs ~profileName:p ~promptForRoots ~prepDebug ();
+    Uicommon.connectRoots
       ~displayWaitMessage:(fun () -> if not reload then displayWaitMessage ())
-      ~getFirstRoot ~getSecondRoot ~prepDebug ~termInteract ();
+      ~termInteract ();
+    profileInitSuccess := true;
     !updateFromProfile ()
   in
 
@@ -3814,35 +4054,41 @@ let createToplevelWindow () =
       | Some n -> n
     in
     clearMainWindow ();
-    if not (Prefs.profileUnchanged ()) then loadProfile n true
-    else Uicommon.refreshConnection ~displayWaitMessage ~termInteract
+    if not (Prefs.profileUnchanged ()) || not (!profileInitSuccess) then
+      loadProfile n true
+    else Uicommon.connectRoots ~displayWaitMessage ~termInteract ()
   in
 
   let detectCmd () =
-    getLock detectUpdatesAndReconcile;
-    updateDetails ();
-    if Prefs.read Globals.batch then begin
-      Prefs.set Globals.batch false; synchronize()
+    if !profileInitSuccess then begin
+      getLock detectUpdatesAndReconcile;
+      updateDetails ();
+      if Prefs.read Globals.batch then begin
+        Prefs.set Globals.batch false; synchronize()
+      end
+    end else begin
+      grSet grRescan true;
+      make_interactive toplevelWindow
     end
   in
 (*  actionBar#insert_space ();*)
   grAdd grRescan
-    (actionBar#insert_button ~text:"Rescan"
-       ~icon:((GMisc.image ~stock:`REFRESH ())#coerce)
+    (insert_button actionBar ~text:"Rescan"
+       ~stock:`REFRESH
        ~tooltip:"Check for updates"
        ~callback: (fun () -> reloadProfile(); detectCmd()) ());
 
   (*********************************************************************
     Profile change button
    *********************************************************************)
-  actionBar#insert_space ();
+  actionBar#insert (GButton.separator_tool_item ());
   let profileChange _ =
     match getProfile false with
       None   -> ()
     | Some p -> clearMainWindow (); loadProfile p false; detectCmd ()
   in
-  grAdd grRescan (actionBar#insert_button ~text:"Change Profile"
-                    ~icon:((GMisc.image ~stock:`OPEN ())#coerce)
+  grAdd grRescan (insert_button actionBar ~text:"Change Profile"
+                    ~stock:`OPEN
                     ~tooltip:"Select a different profile"
                     ~callback:profileChange ());
 
@@ -3921,6 +4167,7 @@ let createToplevelWindow () =
         "Do _Not Propagate Changes" in
     grAdd grAction skip;
     skip#add_accelerator ~group:accel_group ~modi:[`SHIFT] GdkKeysyms._minus;
+    skip#add_accelerator ~group:accel_group GdkKeysyms._KP_Divide;
 
     let merge =
       actionMenu#add_image_item ~key:GdkKeysyms._m ~callback:mergeAction
@@ -3971,7 +4218,7 @@ let createToplevelWindow () =
       (actionMenu#add_item
          ~callback:(fun () ->
             doAction (fun ri _ -> Recon.revertToDefaultDirection ri))
-         "_Revert to Unison's Recommendations");
+         "_Revert to Unison's Recommendation");
     grAdd grAction
       (actionMenu#add_item
          ~callback:(fun () ->
@@ -4061,28 +4308,44 @@ let createToplevelWindow () =
        ~image:(GMisc.image ~stock:`OPEN ~icon_size:`MENU () :> GObj.widget)
        "Change _Profile...");
 
-  let fastProf name key =
-    grAdd grRescan
-      (fileMenu#add_item ~key:key
+  let fastProf i key =
+    let item = fileMenu#add_item ~key:key ~bindname:(string_of_int i) "" in
+    item#misc#hide ();
+    grAdd grRescan item;
+    let show name =
+      match item#children with
+      | [] | _::_::_ -> ()
+      | [l] ->
+          let label = (GMisc.label_cast l) in
+          label#set_label ("Select profile " ^ name);
+          ignore (item#connect#activate
             ~callback:(fun _ ->
                if System.file_exists (Prefs.profilePathname name) then begin
                  Trace.status ("Loading profile " ^ name);
                  loadProfile name false; detectCmd ()
                end else
                  Trace.status ("Profile " ^ name ^ " not found"))
-            ("Select profile " ^ name)) in
+            );
+          item#misc#show ()
+    in
+    (item#misc#hide, show) in
 
   let fastKeysyms =
     [| GdkKeysyms._0; GdkKeysyms._1; GdkKeysyms._2; GdkKeysyms._3;
        GdkKeysyms._4; GdkKeysyms._5; GdkKeysyms._6; GdkKeysyms._7;
        GdkKeysyms._8; GdkKeysyms._9 |] in
 
-  Array.iteri
-    (fun i v -> match v with
-      None -> ()
-    | Some(profile, info) ->
-        fastProf profile fastKeysyms.(i))
-    Uicommon.profileKeymap;
+  let fastKeyitems = Array.init 10 (fun i -> fastProf i fastKeysyms.(i)) in
+
+  let updateProfileKeyMenu () =
+    if !Uicommon.profilesAndRoots = [] then Uicommon.scanProfiles ();
+
+    Array.iteri
+      (fun i v -> match v with
+      | None -> (fst fastKeyitems.(i)) ()
+      | Some (profile, info) -> (snd fastKeyitems.(i)) profile)
+      Uicommon.profileKeymap
+  in
 
   ignore (fileMenu#add_separator ());
   ignore (fileMenu#add_item
@@ -4104,11 +4367,9 @@ let createToplevelWindow () =
     let (expertMenu, _) = add_submenu "Expert" in
 
     let addDebugToggle modname =
-      let cm =
-        expertMenu#add_check_item ~active:(Trace.enabled modname)
-          ~callback:(fun b -> Trace.enable modname b)
-          ("Debug '" ^ modname ^ "'") in
-      cm#set_show_toggle true in
+      ignore (expertMenu#add_check_item ~active:(Trace.enabled modname)
+        ~callback:(fun b -> Trace.enable modname b)
+        ("Debug '" ^ modname ^ "'")) in
 
     addDebugToggle "all";
     addDebugToggle "verbose";
@@ -4134,15 +4395,35 @@ let createToplevelWindow () =
     (fun () ->
        displayNewProfileLabel ();
        setMainWindowColumnHeaders (Uicommon.roots2string ());
+       sizeMainWindow ();
        buildActionMenu false);
 
+  fatalErrorHandler :=
+    (fun err ->
+       grDisactivateAll ();
+       make_interactive toplevelWindow;
+       Trace.status ("Fatal error: " ^ err);
+       inExit := true;
+       fatalError err;
+       inExit := false;
+       match !Prefs.profileName with
+       | Some _ -> grSet grRescan true
+       | None ->  (* Normally should never get here; exceptions loading the
+                     very first profile are handled in the [start] function. *)
+           begin match getProfile true with
+           | None -> exit 1
+           | Some p -> clearMainWindow (); loadProfile p false; detectCmd ()
+           end
+    );
+
 
   ignore (toplevelWindow#event#connect#delete ~callback:
             (fun _ -> safeExit (); true));
   toplevelWindow#show ();
-  fun () ->
-    !updateFromProfile ();
+  fun p ->
+    updateProfileKeyMenu ();
     mainWindow#misc#grab_focus ();
+    loadProfile p false;
     detectCmd ()
 
 
@@ -4151,7 +4432,15 @@ let createToplevelWindow () =
  *********************************************************************)
 
 let start _ =
-  begin try
+  try
+    (* Stop GTK 3 from forcing client-side decorations *)
+    begin
+      try ignore (Unix.getenv "GTK_CSD") with
+      | Unix.Unix_error _ | Not_found ->
+          try Unix.putenv "GTK_CSD" "0" with
+          | Unix.Unix_error _ -> ()
+    end;
+
     (* Initialize the GTK library *)
     ignore (GMain.Main.init ());
 
@@ -4159,10 +4448,9 @@ let start _ =
       Some (fun msg -> warnBox ~parent:(toplevelWindow ()) "Warning" msg);
 
     GtkSignal.user_handler :=
-      (fun exn ->
-         match exn with
-           Util.Transient(s) | Util.Fatal(s) -> fatalError s
-         | exn -> fatalError (Uicommon.exn2string exn));
+      (function
+       | Util.Transient s | Util.Fatal s -> !fatalErrorHandler s
+       | exn -> !fatalErrorHandler (Uicommon.exn2string exn));
 
     (* Ask the Remote module to call us back at regular intervals during
        long network operations. *)
@@ -4172,31 +4460,34 @@ let start _ =
     in
     ignore_result (tick ());
 
-    let prepDebug () =
-      if Sys.os_type = "Win32" then
-        (* As a side-effect, this allocates a console if the process doesn't
-           have one already. This call is here only for the side-effect,
-           because debugging output is produced on stderr and the GUI will
-           crash if there is no stderr. *)
-        try ignore (System.terminalStateFunctions ())
-        with Unix.Unix_error _ -> ()
+    let startGUI = createToplevelWindow () in
+
+    (* Any exceptions here will be caught by the main catch handler
+       and the GUI will exit. *)
+    let getProfile () = match getProfile true with None -> exit 0 | Some x -> x in
+    let profileName =
+      match Uicommon.uiInitClRootsAndProfile ~prepDebug () with
+      | Error s -> begin fatalError s;
+                         Uicommon.clearClRoots (); getProfile () end
+      | Ok None -> getProfile ()
+      | Ok (Some s) -> s
     in
 
-    Os.createUnisonDir();
-    Uicommon.scanProfiles();
-    let detectCmd = createToplevelWindow() in
-
-    Uicommon.uiInit
-      ~prepDebug
-      ~reportError:fatalError
-      ~tryAgainOrQuit
-      ~displayWaitMessage
-      ~getProfile:(fun () -> getProfile true)
-      ~getFirstRoot
-      ~getSecondRoot
-      ~termInteract
-      ();
-    detectCmd ();
+    (* Exceptions from here onwards will be caught by the inner catch handler
+       and the GUI will not exit. Instead, the profile manager is re-opened.
+       User has the option to quit in the profile manager. *)
+    let rec initLoop profileName =
+      try startGUI profileName with
+      | Util.Transient s | Util.Fatal s ->
+          s |> fatalError |> Uicommon.clearClRoots |> getProfile |> initLoop
+      (* Since we have not started the GTK main loop yet, it is easier to
+         handle exceptions here directly. [GtkSignal.safe_call] could be
+         used but it will fail in case of subsequent exceptions without
+         raising, thus escaping further exception handlers.
+         This separate handling sequence could in theory be removed if
+         [startGUI] is called while the GTK main loop is running. *)
+    in
+    initLoop profileName;
 
     (* Display the ui *)
 (*JV: not useful, as Unison does not handle any signal
@@ -4206,9 +4497,8 @@ let start _ =
 *)
     GMain.Main.main ()
   with
-    Util.Transient(s) | Util.Fatal(s) -> fatalError s
-  | exn -> fatalError (Uicommon.exn2string exn)
-  end
+  | Util.Transient s | Util.Fatal s -> fatalError ~quit:true s
+  | exn -> fatalError ~quit:true (Uicommon.exn2string exn)
 
 end (* module Private *)
 
@@ -4225,12 +4515,15 @@ let start = function
       let displayAvailable =
         Util.osType = `Win32
           ||
-        try System.getenv "DISPLAY" <> "" with Not_found -> false
+        (try System.getenv "DISPLAY" <> "" with Not_found -> false)
+          ||
+        (try System.getenv "WAYLAND_DISPLAY" <> "" with Not_found -> false)
       in
       if displayAvailable then Private.start Uicommon.Graphic
-      else
-        Util.warn "DISPLAY not set or empty; starting the Text UI\n";
+      else begin
+        Util.warn "DISPLAY and WAYLAND_DISPLAY not set or empty; starting the Text UI\n";
         Uitext.Body.start Uicommon.Text
+      end
 
 let defaultUi = Uicommon.Graphic
 
diff --git a/src/uigtk2.mli b/src/uigtk3.mli
similarity index 67%
rename from src/uigtk2.mli
rename to src/uigtk3.mli
index 4da1eac..4998e81 100644
--- a/src/uigtk2.mli
+++ b/src/uigtk3.mli
@@ -1,4 +1,4 @@
-(* Unison file synchronizer: src/uigtk2.mli *)
+(* Unison file synchronizer: src/uigtk3.mli *)
 (* Copyright 1999-2020, Benjamin C. Pierce (see COPYING for details) *)
 
 module Body : Uicommon.UI
diff --git a/src/uimac/MyController.m b/src/uimac/MyController.m
index 445c876..a14b84c 100644
--- a/src/uimac/MyController.m
+++ b/src/uimac/MyController.m
@@ -478,10 +478,36 @@ CAMLprim value unisonInit1Complete(value v)
             return;
         }
     }
-    NSLog(@"Unrecognized message from ssh: '%@'",prompt);
-        ocamlCall("x@", "openConnectionCancel", preconn);
-    NSRunAlertPanel(@"Connection Error", @"Unrecognized message from ssh: '%@'", @"OK", nil, nil, prompt);
-    [self chooseProfiles];
+    /* Unison uimac versions <= 2.51.5 always produce an NSLog message
+     * "Calling nonGuiStartup". Previously, this was just hidden from the
+     * user. Starting Unison uimac version 2.51.5, error messages are
+     * displayed to the user. Since this is not an error message, and it
+     * is a known message to ignore, silently drop it here */
+    if (NSEqualRanges([prompt rangeOfString:@"Calling nonGuiStartup"],
+                      NSMakeRange(NSNotFound, 0))) {
+        NSLog(@"Unrecognized message from ssh: '%@'",prompt);
+        NSInteger i = NSRunAlertPanel(@"Connection Error", @"Unrecognized message from ssh: '%@'",
+                          @"Continue", @"Cancel", nil, prompt);
+	if (i == NSAlertAlternateReturn) {
+            ocamlCall("x@", "openConnectionCancel", preconn);
+            [self chooseProfiles];
+            return;
+	}
+    }
+    /* Unrecognized message from ssh does not immediately mean connection
+     * failure. Continue. */
+    prompt = ocamlCall("S@", "openConnectionPrompt", preconn);
+    if (!prompt) {
+        // all done with prompts, finish opening connection
+        ocamlCall("x@", "openConnectionEnd", preconn);
+        waitingForPassword = NO;
+        [self afterOpen];
+        return;
+    } else {
+        [self raisePasswordWindow:[NSString
+            stringWithUTF8String:String_val(Field(prompt, 0))]];
+        return;
+    }
 }
 
 // The password window will invoke this when Enter occurs, b/c we
@@ -934,7 +960,7 @@ CAMLprim value displayStatus(value s)
     [statusText setStringValue:s];
 }
 
-// Called from ocaml to dislpay progress bar
+// Called from ocaml to display progress bar
 CAMLprim value displayGlobalProgress(value p)
 {
   id pool = [[NSAutoreleasePool alloc] init];
diff --git a/src/uimac/cltool.c b/src/uimac/cltool.c
index 2251a5b..5715c33 100644
--- a/src/uimac/cltool.c
+++ b/src/uimac/cltool.c
@@ -1,7 +1,7 @@
 /* cltool.c
 
    This is a command-line tool for Mac OS X that looks up the unison
-   application, whereever it has been installed, and runs it.  This
+   application, wherever it has been installed, and runs it.  This
    is intended to be installed in a standard place (e.g.,
    /usr/bin/unison) to make it easy to invoke unison as a server, or
    to use unison from the command line when it has been installed with
diff --git a/src/uimac/main.m b/src/uimac/main.m
index 8068a61..b51dbee 100644
--- a/src/uimac/main.m
+++ b/src/uimac/main.m
@@ -35,7 +35,7 @@ int main(int argc, const char *argv[])
         !strcmp(argv[i],"-server") ||
         !strcmp(argv[i],"-socket") ||
         !strcmp(argv[i],"-ui")) {
-                        NSLog(@"Calling nonGuiStartup");
+                        //NSLog(@"Calling nonGuiStartup");
                         @try {
                                 ocamlCall("x", "unisonNonGuiStartup");
                         } @catch (NSException *ex) {
diff --git a/src/uimac/uimacnew.xcodeproj/project.pbxproj b/src/uimac/uimacnew.xcodeproj/project.pbxproj
index 09908d3..62b5c05 100644
--- a/src/uimac/uimacnew.xcodeproj/project.pbxproj
+++ b/src/uimac/uimacnew.xcodeproj/project.pbxproj
@@ -496,7 +496,6 @@
 					"-lthreadsnat",
 					"-lcamlstr",
 					"-lasmrun",
-					"$(LIB_BIGARRAY)",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				PRODUCT_NAME = Unison;
@@ -528,7 +527,6 @@
 					"-lthreadsnat",
 					"-lcamlstr",
 					"-lasmrun",
-					"$(LIB_BIGARRAY)",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				PRODUCT_NAME = Unison;
@@ -558,7 +556,6 @@
 					"-lthreadsnat",
 					"-lcamlstr",
 					"-lasmrun",
-					"$(LIB_BIGARRAY)",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				PRODUCT_NAME = Unison;
@@ -603,7 +600,6 @@
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				LIBRARY_SEARCH_PATHS = "";
-				MACOSX_DEPLOYMENT_TARGET = 10.6;
 				ONLY_ACTIVE_ARCH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				SDKROOT = macosx;
@@ -640,7 +636,6 @@
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				LIBRARY_SEARCH_PATHS = "";
-				MACOSX_DEPLOYMENT_TARGET = 10.6;
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				SDKROOT = macosx;
 				SYSTEM_HEADER_SEARCH_PATHS = "$(OCAMLLIBDIR)";
@@ -676,7 +671,6 @@
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				LIBRARY_SEARCH_PATHS = "";
-				MACOSX_DEPLOYMENT_TARGET = 10.6;
 				PRODUCT_BUNDLE_IDENTIFIER = edu.upenn.cis.Unison;
 				SDKROOT = macosx;
 				SYSTEM_HEADER_SEARCH_PATHS = "$(OCAMLLIBDIR)";
diff --git a/src/uimacbridge.ml b/src/uimacbridge.ml
index eb0e4c5..d6912fb 100644
--- a/src/uimacbridge.ml
+++ b/src/uimacbridge.ml
@@ -14,18 +14,102 @@ Callback.register "unisonNonGuiStartup" unisonNonGuiStartup;;
 
 type stateItem = { mutable ri : reconItem;
                    mutable bytesTransferred : Uutil.Filesize.t;
+                   mutable bytesToTransfer : Uutil.Filesize.t;
                    mutable whatHappened : Util.confirmation option;
                    mutable statusMessage : string option };;
 let theState = ref [| |];;
 let unsynchronizedPaths = ref None;;
 
-let unisonDirectory() = System.fspathToPrintString Util.unisonDir
+let unisonDirectory() = Util.unisonDir
 ;;
 Callback.register "unisonDirectory" unisonDirectory;;
 
+(* Global progress indicator, similar to uigtk2.m; *)
+external displayGlobalProgress : float -> unit = "displayGlobalProgress";;
+
+let totalBytesToTransfer = ref Uutil.Filesize.zero;;
+let totalBytesTransferred = ref Uutil.Filesize.zero;;
+
+let lastFrac = ref 0.;;
+let showGlobalProgress b =
+  (* Concatenate the new message *)
+  totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred b;
+  let v =
+    if !totalBytesToTransfer  = Uutil.Filesize.dummy then 0.
+    else if !totalBytesToTransfer  = Uutil.Filesize.zero then 100.
+    else (Uutil.Filesize.percentageOfTotalSize
+       !totalBytesTransferred !totalBytesToTransfer)
+  in
+  if v = 0. || abs_float (v -. !lastFrac) > 1. then begin
+    lastFrac := v;
+    displayGlobalProgress v
+  end;;
+
+let initGlobalProgress b =
+  totalBytesToTransfer := b;
+  totalBytesTransferred := Uutil.Filesize.zero;
+  displayGlobalProgress 0.;;
+
+(* Defined in Bridge.m, used to redisplay the table
+   when the status for a row changes *)
+external bridgeThreadWait : int -> unit = "bridgeThreadWait";;
+
 (* Defined in MyController.m, used to redisplay the table
    when the status for a row changes *)
 external displayStatus : string -> unit = "displayStatus";;
+let displayStatus s = displayStatus (Unicode.protect s);;
+
+(*
+        Called to create callback threads which wait on the C side for callbacks.
+        (We create three just for good measure...)
+
+        FIXME: the thread created by Thread.create doesn't run even if we yield --
+        we have to join.  At that point we actually do get a different pthread, but
+        we've caused the calling thread to block (forever).  As a result, this call
+        never returns.
+*)
+let callbackThreadCreate() =
+        let tCode () =
+                bridgeThreadWait 1;
+        in ignore (Thread.create tCode ()); ignore (Thread.create tCode ());
+        let tid = Thread.create tCode ()
+        in Thread.join tid;
+;;
+Callback.register "callbackThreadCreate" callbackThreadCreate;;
+
+(* Defined in MyController.m; display the error message and exit *)
+external displayFatalError : string -> unit = "fatalError";;
+
+let fatalError message =
+  let () =
+    try Trace.log (message ^ "\n")
+    with Util.Fatal _ -> () in (* Can't allow fatal errors in fatal error handler *)
+  displayFatalError message
+
+(* Defined in MyController.m; display the warning and ask whether to
+   exit or proceed *)
+external displayWarnPanel : string -> bool = "warnPanel";;
+
+let setWarnPrinter() =
+  Util.warnPrinter :=
+    Some(fun s ->
+      Trace.log ("Warning: " ^ s ^ "\n");
+      if not (Prefs.read Globals.batch) then begin
+        if (displayWarnPanel s) then begin
+          Lwt_unix.run (Update.unlockArchives ());
+          exit Uicommon.fatalExit
+        end
+      end)
+
+let doInOtherThread f =
+  Thread.create
+    (fun () ->
+       try
+         f ()
+       with
+         Util.Transient s | Util.Fatal s -> fatalError s
+       | exn -> fatalError (Uicommon.exn2string exn))
+    ()
 
 (* Defined in MyController.m, used to redisplay the table
    when the status for a row changes *)
@@ -33,27 +117,29 @@ external reloadTable : int -> unit = "reloadTable";;
 (* from uigtk2 *)
 let showProgress i bytes dbg =
 (*  Trace.status "showProgress"; *)
-(* XXX There should be a way to reset the amount of bytes transferred... *)
   let i = Uutil.File.toLine i in
   let item = !theState.(i) in
   item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
   let b = item.bytesTransferred in
-  let len = Common.riLength item.ri in
+  let len = item.bytesToTransfer in
   let newstatus =
     if b = Uutil.Filesize.zero || len = Uutil.Filesize.zero then "start "
     else if len = Uutil.Filesize.zero then
       Printf.sprintf "%5s " (Uutil.Filesize.toString b)
     else Util.percent2string (Uutil.Filesize.percentageOfTotalSize b len) in
+  let oldstatus = item.statusMessage in
   item.statusMessage <- Some newstatus;
+  showGlobalProgress bytes;
 (* FIX: No status window in Mac version, see GTK version for how to do it *)
-  reloadTable i;;
+  if oldstatus <> Some newstatus then reloadTable i;;
 
 let unisonGetVersion() = Uutil.myVersion
 ;;
 Callback.register "unisonGetVersion" unisonGetVersion;;
 
 (* snippets from Uicommon, duplicated for now *)
-(* BCP: Duplicating this is a bad idea!!! *)
+(* BCP: Duplicating this is a really bad idea!!! *)
+
 (* First initialization sequence *)
 (* Returns a string option: command line profile, if any *)
 let unisonInit0() =
@@ -65,6 +151,8 @@ let unisonInit0() =
   Trace.sendLogMsgsToStderr := false;
   (* Display progress in GUI *)
   Uutil.setProgressPrinter showProgress;
+  (* Initialise global progress so progress bar is not updated *)
+  initGlobalProgress Uutil.Filesize.dummy;
   (* Make sure we have a directory for archives and profiles *)
   Os.createUnisonDir();
   (* Extract any command line profile or roots *)
@@ -105,37 +193,57 @@ let unisonInit0() =
       let f = Prefs.profilePathname n in
       if not(System.file_exists f)
       then (Printf.eprintf "Profile %s does not exist"
-              (System.fspathToPrintString f);
+              f;
             exit 1)
   end;
   !clprofile
 ;;
 Callback.register "unisonInit0" unisonInit0;;
 
+(* Utility function to tell the UI whether roots were set *)
+
+let areRootsSet () =
+  match Globals.rawRoots() with
+  | [] -> false
+  | _ -> true
+;;
+Callback.register "areRootsSet" areRootsSet;;
+
+(* Utility function to tell the UI whether -batch is set *)
+
+let isBatchSet () =
+  Prefs.read Globals.batch
+;;
+Callback.register "isBatchSet" isBatchSet;;
+
 (* The first time we load preferences, we also read the command line
    arguments; if we re-load prefs (because the user selected a new profile)
    we ignore the command line *)
 let firstTime = ref(true)
 
-(* After figuring out the profile name *)
-let unisonInit1 profileName =
+(* After figuring out the profile name. If the profileName is the empty
+   string, it means that only the roots were specified on the command
+   line *)
+let do_unisonInit1 profileName =
   (* Load the profile and command-line arguments *)
   (* Restore prefs to their default values, if necessary *)
   if not !firstTime then Prefs.resetToDefaults();
   unsynchronizedPaths := None;
 
-  (* Tell the preferences module the name of the profile *)
-  Prefs.profileName := Some(profileName);
+  if profileName <> "" then begin
+    (* Tell the preferences module the name of the profile *)
+    Prefs.profileName := Some(profileName);
 
-  (* If the profile does not exist, create an empty one (this should only
-     happen if the profile is 'default', since otherwise we will already
-     have checked that the named one exists). *)
-   if not(System.file_exists (Prefs.profilePathname profileName)) then
-     Prefs.addComment "Unison preferences file";
+    (* If the profile does not exist, create an empty one (this should only
+       happen if the profile is 'default', since otherwise we will already
+       have checked that the named one exists). *)
+    if not(System.file_exists (Prefs.profilePathname profileName)) then
+      Prefs.addComment "Unison preferences file";
 
-  (* Load the profile *)
-  (Trace.debug "" (fun() -> Util.msg "about to load prefs");
-  Prefs.loadTheFile());
+    (* Load the profile *)
+    (Trace.debug "" (fun() -> Util.msg "about to load prefs");
+    Prefs.loadTheFile())
+  end;
 
   (* Parse the command line.  This will temporarily override
      settings from the profile. *)
@@ -156,7 +264,7 @@ let unisonInit1 profileName =
   let localRoots,remoteRoots =
     Safelist.partition
       (function Clroot.ConnectLocal _ -> true | _ -> false)
-      (Safelist.map Clroot.parseRoot (Globals.rawRoots())) in
+      (Globals.parsedClRawRoots ()) in
 
   match remoteRoots with
     [r] ->
@@ -167,13 +275,28 @@ let unisonInit1 profileName =
     raise(Util.Fatal "cannot synchronize more than one remote root");
   | _ -> None
 ;;
+external unisonInit1Complete : Remote.preconnection option -> unit = "unisonInit1Complete";;
+
+(* Do this in another thread and return immedidately to free up main thread in cocoa *)
+let unisonInit1 profileName =
+  doInOtherThread
+    (fun () ->
+       let r = do_unisonInit1 profileName in
+       unisonInit1Complete r)
+;;
 Callback.register "unisonInit1" unisonInit1;;
 Callback.register "openConnectionPrompt" Remote.openConnectionPrompt;;
 Callback.register "openConnectionReply" Remote.openConnectionReply;;
 Callback.register "openConnectionEnd" Remote.openConnectionEnd;;
 Callback.register "openConnectionCancel" Remote.openConnectionCancel;;
 
-let unisonInit2 () =
+let commitUpdates () =
+  Trace.status "Updating synchronizer state";
+  let t = Trace.startTimer "Updating synchronizer state" in
+  Update.commitUpdates();
+  Trace.showTimer t
+
+let do_unisonInit2 () =
   (* Canonize the names of the roots and install them in Globals. *)
   Globals.installRoots2();
 
@@ -207,6 +330,9 @@ let unisonInit2 () =
        Printf.eprintf "\n"
     );
 
+  (* Install the warning panel, hopefully it's not too late *)
+  setWarnPrinter();
+
   Lwt_unix.run
     (Uicommon.validateAndFixupPrefs () >>=
      Globals.propagatePrefs);
@@ -229,23 +355,28 @@ let unisonInit2 () =
   let t = Trace.startTimer "Checking for updates" in
   let findUpdates () =
     Trace.status "Looking for changes";
-    let updates = Update.findUpdates ~wantWatcher:() !unsynchronizedPaths in
+    let updates = Update.findUpdates ~wantWatcher:true !unsynchronizedPaths in
     Trace.showTimer t;
     updates in
   let reconcile updates = Recon.reconcileAll updates in
   let (reconItemList, thereAreEqualUpdates, dangerousPaths) =
     reconcile (findUpdates ()) in
-  if reconItemList = [] then
+  if not !Update.foundArchives then commitUpdates ();
+  if reconItemList = [] then begin
+    if !Update.foundArchives then commitUpdates ();
     if thereAreEqualUpdates then
-      Trace.status "Replicas have been changed only in identical ways since last sync"
+      Trace.status
+        "Replicas have been changed only in identical ways since last sync"
     else
       Trace.status "Everything is up to date"
-  else
+  end else
     Trace.status "Check and/or adjust selected actions; then press Go";
   Trace.status (Printf.sprintf "There are %d reconitems" (Safelist.length reconItemList));
   let stateItemList =
     Safelist.map
-      (fun ri -> { ri = ri; bytesTransferred = Uutil.Filesize.zero;
+      (fun ri -> { ri = ri;
+                   bytesTransferred = Uutil.Filesize.zero;
+                   bytesToTransfer = Uutil.Filesize.zero;
                    whatHappened = None; statusMessage = None })
       reconItemList in
   theState := Array.of_list stateItemList;
@@ -257,15 +388,29 @@ let unisonInit2 () =
   end;
   !theState
 ;;
+
+external unisonInit2Complete : stateItem array -> unit = "unisonInit2Complete";;
+
+(* Do this in another thread and return immedidately to free up main thread in cocoa *)
+let unisonInit2 () =
+  doInOtherThread
+    (fun () ->
+       let r = do_unisonInit2 () in
+       unisonInit2Complete r)
+;;
 Callback.register "unisonInit2" unisonInit2;;
 
 let unisonRiToDetails ri =
-  match ri.whatHappened with
-    Some (Util.Failed s) -> (Path.toString ri.ri.path1) ^ "\n" ^ s
-  | _ -> (Path.toString ri.ri.path1) ^ "\n" ^ (Uicommon.details2string ri.ri "  ");;
+  Unicode.protect
+    (match ri.whatHappened with
+       Some (Util.Failed s) ->
+         Path.toString ri.ri.path1 ^ "\n" ^ s
+     | _ ->
+         Path.toString ri.ri.path1 ^ "\n" ^
+         Uicommon.details2string ri.ri "  ");;
 Callback.register "unisonRiToDetails" unisonRiToDetails;;
 
-let unisonRiToPath ri = Path.toString ri.ri.path1;;
+let unisonRiToPath ri = Unicode.protect (Path.toString ri.ri.path1);;
 Callback.register "unisonRiToPath" unisonRiToPath;;
 
 let rcToString rc =
@@ -278,16 +423,24 @@ let rcToString rc =
 let unisonRiToLeft ri =
   match ri.ri.replicas with
     Problem _ -> ""
-  | Different diff -> rcToString diff.rc1;;
+  | Different {rc1 = rc} -> rcToString rc;;
 Callback.register "unisonRiToLeft" unisonRiToLeft;;
 let unisonRiToRight ri =
   match ri.ri.replicas with
     Problem _ -> ""
-  | Different diff -> rcToString diff.rc2;;
+  | Different {rc2 = rc} -> rcToString rc;;
 Callback.register "unisonRiToRight" unisonRiToRight;;
 
+let unisonRiToFileSize ri =
+  Uutil.Filesize.toFloat (riLength ri.ri);;
+Callback.register "unisonRiToFileSize" unisonRiToFileSize;;
+
+let unisonRiToFileType ri =
+  riFileType ri.ri;;
+Callback.register "unisonRiToFileType" unisonRiToFileType;;
+
 let direction2niceString = function (* from Uicommon where it's not exported *)
-    Conflict           -> "<-?->"
+    Conflict _         -> "<-?->"
   | Replica1ToReplica2 -> "---->"
   | Replica2ToReplica1 -> "<----"
   | Merge              -> "<-M->"
@@ -310,7 +463,7 @@ Callback.register "unisonRiSetRight" unisonRiSetRight;;
 let unisonRiSetConflict ri =
   match ri.ri.replicas with
     Problem _ -> ()
-  | Different diff -> diff.direction <- Conflict;;
+  | Different diff -> diff.direction <- Conflict "skip requested";;
 Callback.register "unisonRiSetConflict" unisonRiSetConflict;;
 let unisonRiSetMerge ri =
   match ri.ri.replicas with
@@ -327,65 +480,126 @@ Callback.register "unisonRiForceNewer" unisonRiForceNewer;;
 let unisonRiToProgress ri =
   match (ri.statusMessage, ri.whatHappened,ri.ri.replicas) with
     (None,None,_) -> ""
-  | (Some s,None,_) -> s
-  | (_,_,Different {direction = Conflict}) -> ""
+  | (Some s,None,_) -> Unicode.protect s
+  | (_,_,Different {direction = Conflict "files differed"}) -> ""
   | (_,_,Problem _) -> ""
   | (_,Some Util.Succeeded,_) -> "done"
   | (_,Some (Util.Failed s),_) -> "FAILED";;
 Callback.register "unisonRiToProgress" unisonRiToProgress;;
 
-let unisonSynchronize () =
+let unisonRiToBytesTransferred ri =
+  Uutil.Filesize.toFloat ri.bytesTransferred;;
+Callback.register "unisonRiToBytesTransferred" unisonRiToBytesTransferred;;
+
+(* --------------------------------------------------- *)
+
+(* Defined in MyController.m, used to show diffs *)
+external displayDiff : string -> string -> unit = "displayDiff";;
+external displayDiffErr : string -> unit = "displayDiffErr";;
+let displayDiff title text =
+  displayDiff (Unicode.protect title) (Unicode.protect text);;
+let displayDiffErr err = displayDiffErr (Unicode.protect err)
+
+(* If only properties have changed, we can't diff or merge.
+   'Can't diff' is produced (uicommon.ml) if diff is attempted
+   when either side has PropsChanged *)
+let filesAreDifferent status1 status2 =
+  match status1, status2 with
+   `PropsChanged, `Unchanged -> false
+  | `Unchanged, `PropsChanged -> false
+  | `PropsChanged, `PropsChanged -> false
+  | _, _ -> true;;
+
+(* check precondition for diff; used to disable diff button *)
+let canDiff ri =
+  match ri.ri.replicas with
+    Problem _ -> false
+  | Different {rc1 = {typ = `FILE; status = status1};
+               rc2 = {typ = `FILE; status = status2}} ->
+      filesAreDifferent status1 status2
+  | Different _ -> false;;
+Callback.register "canDiff" canDiff;;
+
+(* from Uicommon *)
+(* precondition: uc = File (Updates(_, ..) on both sides *)
+let showDiffs ri printer errprinter id =
+  match ri.replicas with
+    Problem _ ->
+      errprinter
+        "Can't diff files: there was a problem during update detection"
+  | Different
+        {rc1 = {typ = `FILE; status = status1; ui = ui1};
+         rc2 = {typ = `FILE; status = status2; ui = ui2}} ->
+      if filesAreDifferent status1 status2 then
+        (let (root1,root2) = Globals.roots() in
+         begin
+           try Files.diff root1 ri.path1 ui1 root2 ri.path2 ui2 printer id
+           with Util.Transient e -> errprinter e
+         end)
+  | Different _ ->
+      errprinter "Can't diff: path doesn't refer to a file in both replicas"
+
+let runShowDiffs ri i =
+  let file = Uutil.File.ofLine i in
+    showDiffs ri.ri displayDiff displayDiffErr file;;
+Callback.register "runShowDiffs" runShowDiffs;;
+
+(* --------------------------------------------------- *)
+
+let do_unisonSynchronize () =
   if Array.length !theState = 0 then
     Trace.status "Nothing to synchronize"
   else begin
     Trace.status "Propagating changes";
-    Transport.logStart ();
+    Uicommon.transportStart ();
+    let totalLength =
+      Array.fold_left
+        (fun l si ->
+           si.bytesTransferred <- Uutil.Filesize.zero;
+           let len =
+             if si.whatHappened = None then Common.riLength si.ri else
+             Uutil.Filesize.zero
+           in
+           si.bytesToTransfer <- len;
+           Uutil.Filesize.add l len)
+        Uutil.Filesize.zero !theState in
+    initGlobalProgress totalLength;
     let t = Trace.startTimer "Propagating changes" in
-    let im = Array.length !theState in
-    let rec loop i actions pRiThisRound =
-      if i < im then begin
-        let theSI = !theState.(i) in
-        let action =
-          match theSI.whatHappened with
-            None ->
-              if not (pRiThisRound theSI.ri) then
-                return ()
-              else
-                catch (fun () ->
-                         Transport.transportItem
-                           theSI.ri (Uutil.File.ofLine i)
-                           (fun title text ->
-                             Trace.status (Printf.sprintf "MERGE %s: %s" title text); true)
-                         >>= (fun () ->
-                         return Util.Succeeded))
-                      (fun e ->
-                         match e with
-                           Util.Transient s ->
-                             return (Util.Failed s)
-                         | _ ->
-                             fail e)
-                  >>= (fun res ->
-                theSI.whatHappened <- Some res;
-                return ())
-          | Some _ ->
-              return () (* Already processed this one (e.g. merged it) *)
-        in
-        loop (i + 1) (action :: actions) pRiThisRound
-      end else
-        return actions
+    let uiWrapper i theSI =
+      match theSI.whatHappened with
+        None ->
+          catch (fun () ->
+            Transport.transportItem
+              theSI.ri (Uutil.File.ofLine i)
+              (fun title text ->
+                 debug (fun () -> Util.msg "MERGE '%s': '%s'"
+                      title text);
+                 displayDiff title text; true)
+                   >>= (fun () ->
+                   return Util.Succeeded))
+                (fun e ->
+                   match e with
+                     Util.Transient s ->
+                       return (Util.Failed s)
+                   | _ ->
+                       fail e)
+            >>= (fun res ->
+          let rem =
+            Uutil.Filesize.sub
+              theSI.bytesToTransfer theSI.bytesTransferred
+          in
+          if rem <> Uutil.Filesize.zero then
+            showProgress (Uutil.File.ofLine i) rem "done";
+          theSI.whatHappened <- Some res;
+          return ())
+      | Some _ ->
+          return () (* Already processed this one (e.g. merged it) *)
     in
-    Lwt_unix.run
-      (loop 0 [] (fun ri -> not (Common.isDeletion ri)) >>= (fun actions ->
-        Lwt_util.join actions));
-    Lwt_unix.run
-      (loop 0 [] Common.isDeletion >>= (fun actions ->
-        Lwt_util.join actions));
-    Transport.logFinish ();
-    Trace.showTimer t;
-    Trace.status "Updating synchronizer state";
-    let t = Trace.startTimer "Updating synchronizer state" in
-    Update.commitUpdates();
+    Uicommon.transportItems !theState (fun {ri; _} -> not (Common.isDeletion ri)) uiWrapper;
+    Uicommon.transportItems !theState (fun {ri; _} -> Common.isDeletion ri) uiWrapper;
+    Uicommon.transportFinish ();
     Trace.showTimer t;
+    commitUpdates ();
 
     let failureList =
       Array.fold_right
@@ -432,9 +646,9 @@ let unisonSynchronize () =
            match si.ri.replicas with
              Problem err ->
                (si, [err], "error during update detection") :: l
-           | Different diff when diff.direction = Conflict ->
+           | Different diff when (isConflict diff.direction) ->
                (si, [],
-                if diff.default_direction = Conflict then
+                if (isConflict diff.default_direction) then
                   "conflict"
                 else "skipped") :: l
            | _ ->
@@ -453,15 +667,25 @@ let unisonSynchronize () =
     Trace.status
       (Printf.sprintf "Synchronization complete         %s"
          (String.concat ", " (failures @ partials @ skipped)));
+    initGlobalProgress Uutil.Filesize.dummy;
   end;;
+external syncComplete : unit -> unit = "syncComplete";;
+
+(* Do this in another thread and return immedidately to free up main thread in cocoa *)
+let unisonSynchronize () =
+  doInOtherThread
+    (fun () ->
+       do_unisonSynchronize ();
+       syncComplete ())
+;;
 Callback.register "unisonSynchronize" unisonSynchronize;;
 
-let unisonIgnorePath si =
-  Uicommon.addIgnorePattern (Uicommon.ignorePath si.ri.path1);;
-let unisonIgnoreExt si =
-  Uicommon.addIgnorePattern (Uicommon.ignoreExt si.ri.path1);;
-let unisonIgnoreName si =
-  Uicommon.addIgnorePattern (Uicommon.ignoreName si.ri.path1);;
+let unisonIgnorePath pathString =
+  Uicommon.addIgnorePattern (Uicommon.ignorePath (Path.fromString pathString));;
+let unisonIgnoreExt pathString =
+  Uicommon.addIgnorePattern (Uicommon.ignoreExt (Path.fromString pathString));;
+let unisonIgnoreName pathString =
+  Uicommon.addIgnorePattern (Uicommon.ignoreName (Path.fromString pathString));;
 Callback.register "unisonIgnorePath" unisonIgnorePath;;
 Callback.register "unisonIgnoreExt"  unisonIgnoreExt;;
 Callback.register "unisonIgnoreName" unisonIgnoreName;;
@@ -504,10 +728,10 @@ let roots2niceStrings length = function
  | _ -> assert false  (* BOGUS? *);;
 let unisonFirstRootString() =
   let replica1, replica2 = roots2niceStrings 32 (Globals.roots()) in
-  replica1;;
+  Unicode.protect replica1;;
 let unisonSecondRootString() =
   let replica1, replica2 = roots2niceStrings 32 (Globals.roots()) in
-  replica2;;
+  Unicode.protect replica2;;
 Callback.register "unisonFirstRootString" unisonFirstRootString;;
 Callback.register "unisonSecondRootString" unisonSecondRootString;;
 
@@ -516,9 +740,19 @@ Callback.register "unisonSecondRootString" unisonSecondRootString;;
    the current setting is Conflict *)
 let unisonRiIsConflict ri =
   match ri.ri.replicas with
-  | Different {default_direction = Conflict} -> true
+  | Different {default_direction = Conflict "files differ"} -> true
   | _ -> false;;
 Callback.register "unisonRiIsConflict" unisonRiIsConflict;;
+
+(* Test whether reconItem's current state is different from
+   Unison's recommendation.  Used to colour arrows in
+   the reconItems table *)
+let changedFromDefault ri =
+  match ri.ri.replicas with
+    Different diff -> diff.direction <> diff.default_direction
+   | _ -> false;;
+Callback.register "changedFromDefault" changedFromDefault;;
+
 let unisonRiRevert ri =
   match ri.ri.replicas with
   | Different diff -> diff.direction <- diff.default_direction
@@ -534,6 +768,7 @@ let unisonProfileInit (profileName:string) (r1:string) (r2:string) =
 Callback.register "unisonProfileInit" unisonProfileInit;;
 
 Callback.register "unisonPasswordMsg" Terminal.password;;
+Callback.register "unisonPassphraseMsg" Terminal.passphrase;;
 Callback.register "unisonAuthenticityMsg" Terminal.authenticity;;
 
 let unisonExnInfo e =
@@ -543,4 +778,5 @@ let unisonExnInfo e =
   | Unix.Unix_error(ue,s1,s2) ->
       Printf.sprintf "Unix error(%s,%s,%s)" (Unix.error_message ue) s1 s2
   | _ -> Printexc.to_string e;;
-Callback.register "unisonExnInfo" unisonExnInfo;;
+Callback.register "unisonExnInfo"
+  (fun e -> Unicode.protect (unisonExnInfo e));;
diff --git a/src/uimacbridgenew.ml b/src/uimacbridgenew.ml
deleted file mode 100644
index ff97846..0000000
--- a/src/uimacbridgenew.ml
+++ /dev/null
@@ -1,797 +0,0 @@
-(* ML side of a bridge to C for the Mac GUI *)
-
-open Common;;
-open Lwt;;
-
-let debug = Trace.debug "startup"
-
-let unisonNonGuiStartup() = begin
-  (* If there's no GUI, don't print progress in the GUI *)
-  Uutil.setProgressPrinter (fun _ _ _ -> ());
-  Main.nonGuiStartup()    (* If this returns the GUI should be started *)
-end;;
-Callback.register "unisonNonGuiStartup" unisonNonGuiStartup;;
-
-type stateItem = { mutable ri : reconItem;
-                   mutable bytesTransferred : Uutil.Filesize.t;
-                   mutable bytesToTransfer : Uutil.Filesize.t;
-                   mutable whatHappened : Util.confirmation option;
-                   mutable statusMessage : string option };;
-let theState = ref [| |];;
-let unsynchronizedPaths = ref None;;
-
-let unisonDirectory() = System.fspathToString Util.unisonDir
-;;
-Callback.register "unisonDirectory" unisonDirectory;;
-
-(* Global progress indicator, similar to uigtk2.m; *)
-external displayGlobalProgress : float -> unit = "displayGlobalProgress";;
-
-let totalBytesToTransfer = ref Uutil.Filesize.zero;;
-let totalBytesTransferred = ref Uutil.Filesize.zero;;
-
-let lastFrac = ref 0.;;
-let showGlobalProgress b =
-  (* Concatenate the new message *)
-  totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred b;
-  let v =
-    if !totalBytesToTransfer  = Uutil.Filesize.dummy then 0.
-    else if !totalBytesToTransfer  = Uutil.Filesize.zero then 100.
-    else (Uutil.Filesize.percentageOfTotalSize
-       !totalBytesTransferred !totalBytesToTransfer)
-  in
-  if v = 0. || abs_float (v -. !lastFrac) > 1. then begin
-    lastFrac := v;
-    displayGlobalProgress v
-  end;;
-
-let initGlobalProgress b =
-  totalBytesToTransfer := b;
-  totalBytesTransferred := Uutil.Filesize.zero;
-  displayGlobalProgress 0.;;
-
-(* Defined in Bridge.m, used to redisplay the table
-   when the status for a row changes *)
-external bridgeThreadWait : int -> unit = "bridgeThreadWait";;
-
-(* Defined in MyController.m, used to redisplay the table
-   when the status for a row changes *)
-external displayStatus : string -> unit = "displayStatus";;
-let displayStatus s = displayStatus (Unicode.protect s);;
-
-(*
-        Called to create callback threads which wait on the C side for callbacks.
-        (We create three just for good measure...)
-
-        FIXME: the thread created by Thread.create doesn't run even if we yield --
-        we have to join.  At that point we actually do get a different pthread, but
-        we've caused the calling thread to block (forever).  As a result, this call
-        never returns.
-*)
-let callbackThreadCreate() =
-        let tCode () =
-                bridgeThreadWait 1;
-        in ignore (Thread.create tCode ()); ignore (Thread.create tCode ());
-        let tid = Thread.create tCode ()
-        in Thread.join tid;
-;;
-Callback.register "callbackThreadCreate" callbackThreadCreate;;
-
-(* Defined in MyController.m; display the error message and exit *)
-external displayFatalError : string -> unit = "fatalError";;
-
-let fatalError message =
-  let () =
-    try Trace.log (message ^ "\n")
-    with Util.Fatal _ -> () in (* Can't allow fatal errors in fatal error handler *)
-  displayFatalError message
-
-(* Defined in MyController.m; display the warning and ask whether to
-   exit or proceed *)
-external displayWarnPanel : string -> bool = "warnPanel";;
-
-let setWarnPrinter() =
-  Util.warnPrinter :=
-    Some(fun s ->
-      Trace.log ("Warning: " ^ s ^ "\n");
-      if not (Prefs.read Globals.batch) then begin
-        if (displayWarnPanel s) then begin
-          Lwt_unix.run (Update.unlockArchives ());
-          exit Uicommon.fatalExit
-        end
-      end)
-
-let doInOtherThread f =
-  Thread.create
-    (fun () ->
-       try
-         f ()
-       with
-         Util.Transient s | Util.Fatal s -> fatalError s
-       | exn -> fatalError (Uicommon.exn2string exn))
-    ()
-
-(* Defined in MyController.m, used to redisplay the table
-   when the status for a row changes *)
-external reloadTable : int -> unit = "reloadTable";;
-(* from uigtk2 *)
-let showProgress i bytes dbg =
-(*  Trace.status "showProgress"; *)
-  let i = Uutil.File.toLine i in
-  let item = !theState.(i) in
-  item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
-  let b = item.bytesTransferred in
-  let len = item.bytesToTransfer in
-  let newstatus =
-    if b = Uutil.Filesize.zero || len = Uutil.Filesize.zero then "start "
-    else if len = Uutil.Filesize.zero then
-      Printf.sprintf "%5s " (Uutil.Filesize.toString b)
-    else Util.percent2string (Uutil.Filesize.percentageOfTotalSize b len) in
-  let oldstatus = item.statusMessage in
-  item.statusMessage <- Some newstatus;
-  showGlobalProgress bytes;
-(* FIX: No status window in Mac version, see GTK version for how to do it *)
-  if oldstatus <> Some newstatus then reloadTable i;;
-
-let unisonGetVersion() = Uutil.myVersion
-;;
-Callback.register "unisonGetVersion" unisonGetVersion;;
-
-(* snippets from Uicommon, duplicated for now *)
-(* BCP: Duplicating this is a really bad idea!!! *)
-
-(* First initialization sequence *)
-(* Returns a string option: command line profile, if any *)
-let unisonInit0() =
-  ignore (Gc.set {(Gc.get ()) with Gc.max_overhead = 150});
-  (* Display status in GUI instead of on stderr *)
-  let formatStatus major minor = (Util.padto 30 (major ^ "  ")) ^ minor in
-  Trace.messageDisplayer := displayStatus;
-  Trace.statusFormatter := formatStatus;
-  Trace.sendLogMsgsToStderr := false;
-  (* Display progress in GUI *)
-  Uutil.setProgressPrinter showProgress;
-  (* Initialise global progress so progress bar is not updated *)
-  initGlobalProgress Uutil.Filesize.dummy;
-  (* Make sure we have a directory for archives and profiles *)
-  Os.createUnisonDir();
-  (* Extract any command line profile or roots *)
-  let clprofile = ref None in
-  begin
-    try
-      let args = Prefs.scanCmdLine Uicommon.usageMsg in
-      match Util.StringMap.find "rest" args with
-        [] -> ()
-      | [profile] -> clprofile := Some profile
-      | [root2;root1] -> Globals.setRawRoots [root1;root2]
-      | [root2;root1;profile] ->
-          Globals.setRawRoots [root1;root2];
-          clprofile := Some profile
-      | _ ->
-          (Printf.eprintf
-             "%s was invoked incorrectly (too many roots)" Uutil.myName;
-           exit 1)
-    with Not_found -> ()
-  end;
-  (* Print header for debugging output *)
-  debug (fun() ->
-    Printf.eprintf "%s, version %s\n\n" Uutil.myName Uutil.myVersion);
-  debug (fun() -> Util.msg "initializing UI");
-  debug (fun () ->
-    (match !clprofile with
-      None -> Util.msg "No profile given on command line"
-    | Some s -> Printf.eprintf "Profile '%s' given on command line" s);
-    (match Globals.rawRoots() with
-      [] -> Util.msg "No roots given on command line"
-    | [root1;root2] ->
-        Printf.eprintf "Roots '%s' and '%s' given on command line"
-          root1 root2
-    | _ -> assert false));
-  begin match !clprofile with
-    None -> ()
-  | Some n ->
-      let f = Prefs.profilePathname n in
-      if not(System.file_exists f)
-      then (Printf.eprintf "Profile %s does not exist"
-              (System.fspathToPrintString f);
-            exit 1)
-  end;
-  !clprofile
-;;
-Callback.register "unisonInit0" unisonInit0;;
-
-(* Utility function to tell the UI whether roots were set *)
-
-let areRootsSet () =
-  match Globals.rawRoots() with
-  | [] -> false
-  | _ -> true
-;;
-Callback.register "areRootsSet" areRootsSet;;
-
-(* Utility function to tell the UI whether -batch is set *)
-
-let isBatchSet () =
-  Prefs.read Globals.batch
-;;
-Callback.register "isBatchSet" isBatchSet;;
-
-(* The first time we load preferences, we also read the command line
-   arguments; if we re-load prefs (because the user selected a new profile)
-   we ignore the command line *)
-let firstTime = ref(true)
-
-(* After figuring out the profile name. If the profileName is the empty
-   string, it means that only the roots were specified on the command
-   line *)
-let do_unisonInit1 profileName =
-  (* Load the profile and command-line arguments *)
-  (* Restore prefs to their default values, if necessary *)
-  if not !firstTime then Prefs.resetToDefaults();
-  unsynchronizedPaths := None;
-
-  if profileName <> "" then begin
-    (* Tell the preferences module the name of the profile *)
-    Prefs.profileName := Some(profileName);
-
-    (* If the profile does not exist, create an empty one (this should only
-       happen if the profile is 'default', since otherwise we will already
-       have checked that the named one exists). *)
-    if not(System.file_exists (Prefs.profilePathname profileName)) then
-      Prefs.addComment "Unison preferences file";
-
-    (* Load the profile *)
-    (Trace.debug "" (fun() -> Util.msg "about to load prefs");
-    Prefs.loadTheFile())
-  end;
-
-  (* Parse the command line.  This will temporarily override
-     settings from the profile. *)
-  if !firstTime then begin
-    Trace.debug "" (fun() -> Util.msg "about to parse command line");
-    Prefs.parseCmdLine Uicommon.usageMsg;
-  end;
-
-  firstTime := false;
-
-  (* Print the preference settings *)
-  Trace.debug "" (fun() -> Prefs.dumpPrefsToStderr() );
-
-  (* FIX: if no roots, ask the user *)
-
-  Recon.checkThatPreferredRootIsValid();
-
-  let localRoots,remoteRoots =
-    Safelist.partition
-      (function Clroot.ConnectLocal _ -> true | _ -> false)
-      (Safelist.map Clroot.parseRoot (Globals.rawRoots())) in
-
-  match remoteRoots with
-    [r] ->
-      (* FIX: tell the user the next step (contacting server) might
-         take a while *)
-      Remote.openConnectionStart r
-  | _::_::_ ->
-    raise(Util.Fatal "cannot synchronize more than one remote root");
-  | _ -> None
-;;
-external unisonInit1Complete : Remote.preconnection option -> unit = "unisonInit1Complete";;
-
-(* Do this in another thread and return immedidately to free up main thread in cocoa *)
-let unisonInit1 profileName =
-  doInOtherThread
-    (fun () ->
-       let r = do_unisonInit1 profileName in
-       unisonInit1Complete r)
-;;
-Callback.register "unisonInit1" unisonInit1;;
-Callback.register "openConnectionPrompt" Remote.openConnectionPrompt;;
-Callback.register "openConnectionReply" Remote.openConnectionReply;;
-Callback.register "openConnectionEnd" Remote.openConnectionEnd;;
-Callback.register "openConnectionCancel" Remote.openConnectionCancel;;
-
-let commitUpdates () =
-  Trace.status "Updating synchronizer state";
-  let t = Trace.startTimer "Updating synchronizer state" in
-  Update.commitUpdates();
-  Trace.showTimer t
-
-let do_unisonInit2 () =
-  (* Canonize the names of the roots and install them in Globals. *)
-  Globals.installRoots2();
-
-  (* If both roots are local, disable the xferhint table to save time *)
-  begin match Globals.roots() with
-    ((Local,_),(Local,_)) -> Prefs.set Xferhint.xferbycopying false
-  | _ -> ()
-  end;
-
-  (* If no paths were specified, then synchronize the whole replicas *)
-  if Prefs.read Globals.paths = [] then Prefs.set Globals.paths [Path.empty];
-
-  (* Expand any "wildcard" paths [with final component *] *)
-  Globals.expandWildcardPaths();
-
-  Update.storeRootsName ();
-
-  Trace.debug ""
-    (fun() ->
-       Printf.eprintf "Roots: \n";
-       Safelist.iter (fun clr -> Printf.eprintf "        %s\n" clr)
-         (Globals.rawRoots ());
-       Printf.eprintf "  i.e. \n";
-       Safelist.iter (fun clr -> Printf.eprintf "        %s\n"
-                    (Clroot.clroot2string (Clroot.parseRoot clr)))
-         (Globals.rawRoots ());
-       Printf.eprintf "  i.e. (in canonical order)\n";
-       Safelist.iter (fun r ->
-         Printf.eprintf "       %s\n" (root2string r))
-         (Globals.rootsInCanonicalOrder());
-       Printf.eprintf "\n"
-    );
-
-  (* Install the warning panel, hopefully it's not too late *)
-  setWarnPrinter();
-
-  Lwt_unix.run
-    (Uicommon.validateAndFixupPrefs () >>=
-     Globals.propagatePrefs);
-
-  (* Initializes some backups stuff according to the preferences just loaded from the profile.
-     Important to do it here, after prefs are propagated, because the function will also be
-     run on the server, if any. Also, this should be done each time a profile is reloaded
-     on this side, that's why it's here. *)
-  Stasher.initBackups ();
-
-  (* Turn on GC messages, if the '-debug gc' flag was provided *)
-  if Trace.enabled "gc" then Gc.set {(Gc.get ()) with Gc.verbose = 0x3F};
-
-  (* BCPFIX: Should/can this be done earlier?? *)
-  Files.processCommitLogs();
-
-  (* from Uigtk2 *)
-  (* detect updates and reconcile *)
-  let _ = Globals.roots () in
-  let t = Trace.startTimer "Checking for updates" in
-  let findUpdates () =
-    Trace.status "Looking for changes";
-    let updates = Update.findUpdates ~wantWatcher:() !unsynchronizedPaths in
-    Trace.showTimer t;
-    updates in
-  let reconcile updates = Recon.reconcileAll updates in
-  let (reconItemList, thereAreEqualUpdates, dangerousPaths) =
-    reconcile (findUpdates ()) in
-  if not !Update.foundArchives then commitUpdates ();
-  if reconItemList = [] then begin
-    if !Update.foundArchives then commitUpdates ();
-    if thereAreEqualUpdates then
-      Trace.status
-        "Replicas have been changed only in identical ways since last sync"
-    else
-      Trace.status "Everything is up to date"
-  end else
-    Trace.status "Check and/or adjust selected actions; then press Go";
-  Trace.status (Printf.sprintf "There are %d reconitems" (Safelist.length reconItemList));
-  let stateItemList =
-    Safelist.map
-      (fun ri -> { ri = ri;
-                   bytesTransferred = Uutil.Filesize.zero;
-                   bytesToTransfer = Uutil.Filesize.zero;
-                   whatHappened = None; statusMessage = None })
-      reconItemList in
-  theState := Array.of_list stateItemList;
-  unsynchronizedPaths :=
-    Some (Safelist.map (fun ri -> ri.path1) reconItemList, []);
-  if dangerousPaths <> [] then begin
-    Prefs.set Globals.batch false;
-    Util.warn (Uicommon.dangerousPathMsg dangerousPaths)
-  end;
-  !theState
-;;
-
-external unisonInit2Complete : stateItem array -> unit = "unisonInit2Complete";;
-
-(* Do this in another thread and return immedidately to free up main thread in cocoa *)
-let unisonInit2 () =
-  doInOtherThread
-    (fun () ->
-       let r = do_unisonInit2 () in
-       unisonInit2Complete r)
-;;
-Callback.register "unisonInit2" unisonInit2;;
-
-let unisonRiToDetails ri =
-  Unicode.protect
-    (match ri.whatHappened with
-       Some (Util.Failed s) ->
-         Path.toString ri.ri.path1 ^ "\n" ^ s
-     | _ ->
-         Path.toString ri.ri.path1 ^ "\n" ^
-         Uicommon.details2string ri.ri "  ");;
-Callback.register "unisonRiToDetails" unisonRiToDetails;;
-
-let unisonRiToPath ri = Unicode.protect (Path.toString ri.ri.path1);;
-Callback.register "unisonRiToPath" unisonRiToPath;;
-
-let rcToString rc =
-  match rc.status with
-    `Deleted      -> "Deleted"
-  | `Modified     -> "Modified"
-  | `PropsChanged -> "PropsChanged"
-  | `Created      -> "Created"
-  | `Unchanged    -> "";;
-let unisonRiToLeft ri =
-  match ri.ri.replicas with
-    Problem _ -> ""
-  | Different {rc1 = rc} -> rcToString rc;;
-Callback.register "unisonRiToLeft" unisonRiToLeft;;
-let unisonRiToRight ri =
-  match ri.ri.replicas with
-    Problem _ -> ""
-  | Different {rc2 = rc} -> rcToString rc;;
-Callback.register "unisonRiToRight" unisonRiToRight;;
-
-let unisonRiToFileSize ri =
-  Uutil.Filesize.toFloat (riLength ri.ri);;
-Callback.register "unisonRiToFileSize" unisonRiToFileSize;;
-
-let unisonRiToFileType ri =
-  riFileType ri.ri;;
-Callback.register "unisonRiToFileType" unisonRiToFileType;;
-
-let direction2niceString = function (* from Uicommon where it's not exported *)
-    Conflict _         -> "<-?->"
-  | Replica1ToReplica2 -> "---->"
-  | Replica2ToReplica1 -> "<----"
-  | Merge              -> "<-M->"
-let unisonRiToDirection ri =
-  match ri.ri.replicas with
-    Problem _ -> "XXXXX"
-  | Different diff -> direction2niceString diff.direction;;
-Callback.register "unisonRiToDirection" unisonRiToDirection;;
-
-let unisonRiSetLeft ri =
-  match ri.ri.replicas with
-    Problem _ -> ()
-  | Different diff -> diff.direction <- Replica2ToReplica1;;
-Callback.register "unisonRiSetLeft" unisonRiSetLeft;;
-let unisonRiSetRight ri =
-  match ri.ri.replicas with
-    Problem _ -> ()
-  | Different diff -> diff.direction <- Replica1ToReplica2;;
-Callback.register "unisonRiSetRight" unisonRiSetRight;;
-let unisonRiSetConflict ri =
-  match ri.ri.replicas with
-    Problem _ -> ()
-  | Different diff -> diff.direction <- Conflict "skip requested";;
-Callback.register "unisonRiSetConflict" unisonRiSetConflict;;
-let unisonRiSetMerge ri =
-  match ri.ri.replicas with
-    Problem _ -> ()
-  | Different diff -> diff.direction <- Merge;;
-Callback.register "unisonRiSetMerge" unisonRiSetMerge;;
-let unisonRiForceOlder ri =
-  Recon.setDirection ri.ri `Older `Force;;
-Callback.register "unisonRiForceOlder" unisonRiForceOlder;;
-let unisonRiForceNewer ri =
-  Recon.setDirection ri.ri `Newer `Force;;
-Callback.register "unisonRiForceNewer" unisonRiForceNewer;;
-
-let unisonRiToProgress ri =
-  match (ri.statusMessage, ri.whatHappened,ri.ri.replicas) with
-    (None,None,_) -> ""
-  | (Some s,None,_) -> Unicode.protect s
-  | (_,_,Different {direction = Conflict "files differed"}) -> ""
-  | (_,_,Problem _) -> ""
-  | (_,Some Util.Succeeded,_) -> "done"
-  | (_,Some (Util.Failed s),_) -> "FAILED";;
-Callback.register "unisonRiToProgress" unisonRiToProgress;;
-
-let unisonRiToBytesTransferred ri =
-  Uutil.Filesize.toFloat ri.bytesTransferred;;
-Callback.register "unisonRiToBytesTransferred" unisonRiToBytesTransferred;;
-
-(* --------------------------------------------------- *)
-
-(* Defined in MyController.m, used to show diffs *)
-external displayDiff : string -> string -> unit = "displayDiff";;
-external displayDiffErr : string -> unit = "displayDiffErr";;
-let displayDiff title text =
-  displayDiff (Unicode.protect title) (Unicode.protect text);;
-let displayDiffErr err = displayDiffErr (Unicode.protect err)
-
-(* If only properties have changed, we can't diff or merge.
-   'Can't diff' is produced (uicommon.ml) if diff is attemped
-   when either side has PropsChanged *)
-let filesAreDifferent status1 status2 =
-  match status1, status2 with
-   `PropsChanged, `Unchanged -> false
-  | `Unchanged, `PropsChanged -> false
-  | `PropsChanged, `PropsChanged -> false
-  | _, _ -> true;;
-
-(* check precondition for diff; used to disable diff button *)
-let canDiff ri =
-  match ri.ri.replicas with
-    Problem _ -> false
-  | Different {rc1 = {typ = `FILE; status = status1};
-               rc2 = {typ = `FILE; status = status2}} ->
-      filesAreDifferent status1 status2
-  | Different _ -> false;;
-Callback.register "canDiff" canDiff;;
-
-(* from Uicommon *)
-(* precondition: uc = File (Updates(_, ..) on both sides *)
-let showDiffs ri printer errprinter id =
-  match ri.replicas with
-    Problem _ ->
-      errprinter
-        "Can't diff files: there was a problem during update detection"
-  | Different
-        {rc1 = {typ = `FILE; status = status1; ui = ui1};
-         rc2 = {typ = `FILE; status = status2; ui = ui2}} ->
-      if filesAreDifferent status1 status2 then
-        (let (root1,root2) = Globals.roots() in
-         begin
-           try Files.diff root1 ri.path1 ui1 root2 ri.path2 ui2 printer id
-           with Util.Transient e -> errprinter e
-         end)
-  | Different _ ->
-      errprinter "Can't diff: path doesn't refer to a file in both replicas"
-
-let runShowDiffs ri i =
-  let file = Uutil.File.ofLine i in
-    showDiffs ri.ri displayDiff displayDiffErr file;;
-Callback.register "runShowDiffs" runShowDiffs;;
-
-(* --------------------------------------------------- *)
-
-let do_unisonSynchronize () =
-  if Array.length !theState = 0 then
-    Trace.status "Nothing to synchronize"
-  else begin
-    Trace.status "Propagating changes";
-    Transport.logStart ();
-    let totalLength =
-      Array.fold_left
-        (fun l si ->
-           si.bytesTransferred <- Uutil.Filesize.zero;
-           let len =
-             if si.whatHappened = None then Common.riLength si.ri else
-             Uutil.Filesize.zero
-           in
-           si.bytesToTransfer <- len;
-           Uutil.Filesize.add l len)
-        Uutil.Filesize.zero !theState in
-    initGlobalProgress totalLength;
-    let t = Trace.startTimer "Propagating changes" in
-    let im = Array.length !theState in
-    let rec loop i actions pRiThisRound =
-      if i < im then begin
-        let theSI = !theState.(i) in
-        let action =
-          match theSI.whatHappened with
-            None ->
-              if not (pRiThisRound theSI.ri) then
-                return ()
-              else
-                catch (fun () ->
-                  Transport.transportItem
-                    theSI.ri (Uutil.File.ofLine i)
-                    (fun title text ->
-                       debug (fun () -> Util.msg "MERGE '%s': '%s'"
-                            title text);
-                       displayDiff title text; true)
-                         >>= (fun () ->
-                         return Util.Succeeded))
-                      (fun e ->
-                         match e with
-                           Util.Transient s ->
-                             return (Util.Failed s)
-                         | _ ->
-                             fail e)
-                  >>= (fun res ->
-                let rem =
-                  Uutil.Filesize.sub
-                    theSI.bytesToTransfer theSI.bytesTransferred
-                in
-                if rem <> Uutil.Filesize.zero then
-                  showProgress (Uutil.File.ofLine i) rem "done";
-                theSI.whatHappened <- Some res;
-                return ())
-          | Some _ ->
-              return () (* Already processed this one (e.g. merged it) *)
-        in
-        loop (i + 1) (action :: actions) pRiThisRound
-      end else
-        return actions
-    in
-    Lwt_unix.run
-      (loop 0 [] (fun ri -> not (Common.isDeletion ri)) >>= (fun actions ->
-        Lwt_util.join actions));
-    Lwt_unix.run
-      (loop 0 [] Common.isDeletion >>= (fun actions ->
-        Lwt_util.join actions));
-    Transport.logFinish ();
-    Trace.showTimer t;
-    commitUpdates ();
-
-    let failureList =
-      Array.fold_right
-        (fun si l ->
-           match si.whatHappened with
-             Some (Util.Failed err) ->
-               (si, [err], "transport failure") :: l
-           | _ ->
-               l)
-        !theState []
-    in
-    let failureCount = List.length failureList in
-    let failures =
-      if failureCount = 0 then [] else
-      [Printf.sprintf "%d failure%s"
-         failureCount (if failureCount = 1 then "" else "s")]
-    in
-    let partialList =
-      Array.fold_right
-        (fun si l ->
-           match si.whatHappened with
-             Some Util.Succeeded
-             when partiallyProblematic si.ri &&
-                  not (problematic si.ri) ->
-               let errs =
-                 match si.ri.replicas with
-                   Different diff -> diff.errors1 @ diff.errors2
-                 | _              -> assert false
-               in
-               (si, errs,
-                "partial transfer (errors during update detection)") :: l
-           | _ ->
-               l)
-        !theState []
-    in
-    let partialCount = List.length partialList in
-    let partials =
-      if partialCount = 0 then [] else
-      [Printf.sprintf "%d partially transferred" partialCount]
-    in
-    let skippedList =
-      Array.fold_right
-        (fun si l ->
-           match si.ri.replicas with
-             Problem err ->
-               (si, [err], "error during update detection") :: l
-           | Different diff when (isConflict diff.direction) ->
-               (si, [],
-                if (isConflict diff.default_direction) then
-                  "conflict"
-                else "skipped") :: l
-           | _ ->
-               l)
-        !theState []
-    in
-    let skippedCount = List.length skippedList in
-    let skipped =
-      if skippedCount = 0 then [] else
-      [Printf.sprintf "%d skipped" skippedCount]
-    in
-    unsynchronizedPaths :=
-      Some (Safelist.map (fun (si, _, _) -> si.ri.path1)
-              (failureList @ partialList @ skippedList),
-            []);
-    Trace.status
-      (Printf.sprintf "Synchronization complete         %s"
-         (String.concat ", " (failures @ partials @ skipped)));
-    initGlobalProgress Uutil.Filesize.dummy;
-  end;;
-external syncComplete : unit -> unit = "syncComplete";;
-
-(* Do this in another thread and return immedidately to free up main thread in cocoa *)
-let unisonSynchronize () =
-  doInOtherThread
-    (fun () ->
-       do_unisonSynchronize ();
-       syncComplete ())
-;;
-Callback.register "unisonSynchronize" unisonSynchronize;;
-
-let unisonIgnorePath pathString =
-  Uicommon.addIgnorePattern (Uicommon.ignorePath (Path.fromString pathString));;
-let unisonIgnoreExt pathString =
-  Uicommon.addIgnorePattern (Uicommon.ignoreExt (Path.fromString pathString));;
-let unisonIgnoreName pathString =
-  Uicommon.addIgnorePattern (Uicommon.ignoreName (Path.fromString pathString));;
-Callback.register "unisonIgnorePath" unisonIgnorePath;;
-Callback.register "unisonIgnoreExt"  unisonIgnoreExt;;
-Callback.register "unisonIgnoreName" unisonIgnoreName;;
-
-(* Update the state to take into account ignore patterns.
-   Return the new index of the first state item that is
-   not ignored starting at old index i.
-*)
-let unisonUpdateForIgnore i =
-  let l = ref [] in
-  let num = ref(-1) in
-  let newI = ref None in
-  (* FIX: we should actually test whether any prefix is now ignored *)
-  let keep s = not (Globals.shouldIgnore s.ri.path1) in
-  for j = 0 to (Array.length !theState - 1) do
-    let s = !theState.(j) in
-    if keep s then begin
-      l := s :: !l;
-      num := !num + 1;
-      if (j>=i && !newI=None) then newI := Some !num
-    end
-  done;
-  theState := Array.of_list (Safelist.rev !l);
-  match !newI with None -> (Array.length !theState - 1)
-  | Some i' -> i';;
-Callback.register "unisonUpdateForIgnore" unisonUpdateForIgnore;;
-
-let unisonState () = !theState;;
-Callback.register "unisonState" unisonState;;
-
-(* from Uicommon *)
-let roots2niceStrings length = function
-   (Local,fspath1), (Local,fspath2) ->
-    let name1, name2 = Fspath.differentSuffix fspath1 fspath2 in
-    (Util.truncateString name1 length, Util.truncateString name2 length)
- | (Local,fspath1), (Remote host, fspath2) ->
-    (Util.truncateString "local" length, Util.truncateString host length)
- | (Remote host, fspath1), (Local,fspath2) ->
-    (Util.truncateString host length, Util.truncateString "local" length)
- | _ -> assert false  (* BOGUS? *);;
-let unisonFirstRootString() =
-  let replica1, replica2 = roots2niceStrings 32 (Globals.roots()) in
-  Unicode.protect replica1;;
-let unisonSecondRootString() =
-  let replica1, replica2 = roots2niceStrings 32 (Globals.roots()) in
-  Unicode.protect replica2;;
-Callback.register "unisonFirstRootString" unisonFirstRootString;;
-Callback.register "unisonSecondRootString" unisonSecondRootString;;
-
-
-(* Note, this returns whether the files conflict, NOT whether
-   the current setting is Conflict *)
-let unisonRiIsConflict ri =
-  match ri.ri.replicas with
-  | Different {default_direction = Conflict "files differ"} -> true
-  | _ -> false;;
-Callback.register "unisonRiIsConflict" unisonRiIsConflict;;
-
-(* Test whether reconItem's current state is different from
-   Unison's recommendation.  Used to colour arrows in
-   the reconItems table *)
-let changedFromDefault ri =
-  match ri.ri.replicas with
-    Different diff -> diff.direction <> diff.default_direction
-   | _ -> false;;
-Callback.register "changedFromDefault" changedFromDefault;;
-
-let unisonRiRevert ri =
-  match ri.ri.replicas with
-  | Different diff -> diff.direction <- diff.default_direction
-  | _ -> ();;
-Callback.register "unisonRiRevert" unisonRiRevert;;
-
-let unisonProfileInit (profileName:string) (r1:string) (r2:string) =
-  Prefs.resetToDefaults();
-  Prefs.profileName := Some(profileName);
-  Prefs.addComment "Unison preferences file"; (* Creates the file, assumes it doesn't exist *)
-  ignore (Prefs.add "root" r1);
-  ignore (Prefs.add "root" r2);;
-Callback.register "unisonProfileInit" unisonProfileInit;;
-
-Callback.register "unisonPasswordMsg" Terminal.password;;
-Callback.register "unisonPassphraseMsg" Terminal.passphrase;;
-Callback.register "unisonAuthenticityMsg" Terminal.authenticity;;
-
-let unisonExnInfo e =
-  match e with
-    Util.Fatal s -> Printf.sprintf "Fatal error: %s" s
-  | Invalid_argument s -> Printf.sprintf "Invalid argument: %s" s
-  | Unix.Unix_error(ue,s1,s2) ->
-      Printf.sprintf "Unix error(%s,%s,%s)" (Unix.error_message ue) s1 s2
-  | _ -> Printexc.to_string e;;
-Callback.register "unisonExnInfo"
-  (fun e -> Unicode.protect (unisonExnInfo e));;
diff --git a/src/uitext.ml b/src/uitext.ml
index 6b5b666..5d1513a 100644
--- a/src/uitext.ml
+++ b/src/uitext.ml
@@ -25,7 +25,8 @@ let debug = Trace.debug "ui"
 let dumbtty =
   Prefs.createBool "dumbtty"
     (try System.getenv "EMACS" <> "" with Not_found -> false)
-    "!do not change terminal settings in text UI"
+    ~category:(`Advanced `CLI)
+    "do not change terminal settings in text UI"
     ("When set to \\verb|true|, this flag makes the text mode user "
      ^ "interface avoid trying to change any of the terminal settings.  "
      ^ "(Normally, Unison puts the terminal in `raw mode', so that it can "
@@ -40,7 +41,9 @@ let dumbtty =
      ^ "interface.")
 
 let silent =
-  Prefs.createBool "silent" false "print nothing except error messages"
+  Prefs.createBool "silent" false
+    ~category:(`Basic `Syncprocess_CLI)
+    "print nothing except error messages"
     ("When this preference is set to {\\tt true}, the textual user "
      ^ "interface will print nothing at all, except in the case of errors.  "
      ^ "Setting \\texttt{silent} to true automatically sets the "
@@ -87,8 +90,9 @@ let setupTerminal() =
       restoreTerminal ()
 
 let colorMode =
-  Prefs.createBoolWithDefault "color" ~local:true
-    "!use color output for text UI (true/false/default)"
+  Prefs.createBoolWithDefault "color"
+    ~category:(`Advanced `CLI) ~local:true
+    "use color output for text UI (true/false/default)"
     ("When set to {\\tt true}, this flag enables color output in "
      ^ "text mode user interface. When set to {\\tt false}, all "
      ^ "color output is disabled. Default is to enable color if "
@@ -100,13 +104,15 @@ let setColorPreference () =
   let envOk = try let _ = System.getenv "NO_COLOR" in false
     with Not_found -> true
   and termOk = try System.getenv "TERM" <> "dumb" with Not_found -> true
-  and ttyOk = (Unix.isatty Unix.stdin) && (Unix.isatty Unix.stderr) in
+  and ttyOk = (Unix.isatty Unix.stdout) && (Unix.isatty Unix.stderr) in
   let colorOk = envOk && termOk && ttyOk && not (Prefs.read dumbtty) in
   colorEnabled :=
     match Prefs.read colorMode with
     | `True    -> true
     | `False   -> false
-    | `Default -> colorOk && Sys.os_type <> "Win32"
+    | `Default -> colorOk && (not Sys.win32
+                    || (System.termVtCapable Unix.stdout
+                        && System.termVtCapable Unix.stderr))
 
 let color t =
   if not !colorEnabled then "" else
@@ -461,7 +467,7 @@ let interact prilist rilist =
           begin match !Prefs.profileName with None -> assert false |
             Some(n) ->
               display ("  To un-ignore, edit "
-                       ^ System.fspathToPrintString (Prefs.profilePathname n)
+                       ^ Prefs.profilePathname n
                        ^ " and restart " ^ Uutil.myName ^ "\n") end;
           let nukeIgnoredRis =
             Safelist.filter (fun ri -> not (Globals.shouldIgnore ri.path1)) in
@@ -781,7 +787,7 @@ type stateItem =
     mutable bytesTransferred : Uutil.Filesize.t;
     mutable bytesToTransfer : Uutil.Filesize.t }
 
-let doTransport reconItemList =
+let doTransport reconItemList numskip isSkip =
   let items =
     Array.map
       (fun ri ->
@@ -790,57 +796,138 @@ let doTransport reconItemList =
           bytesToTransfer = Common.riLength ri})
       (Array.of_list reconItemList)
   in
+  let totalItemsTransferred = ref 0 in
+  let totalItemsToTransfer = Array.length items - numskip in
+  let totalItemsToTransferStr = string_of_int totalItemsToTransfer in
   let totalBytesTransferred = ref Uutil.Filesize.zero in
   let totalBytesToTransfer =
-    ref
       (Array.fold_left
          (fun s item -> Uutil.Filesize.add item.bytesToTransfer s)
          Uutil.Filesize.zero items)
   in
-  let t0 = Unix.gettimeofday () in
+  let totalBytesToTransferStr = Util.bytes2string
+    (Uutil.Filesize.toInt64 totalBytesToTransfer) in
+  let totalToTransfer =
+    Uutil.Filesize.(add totalBytesToTransfer (ofInt totalItemsToTransfer)) in
+  let sta = Uicommon.Stats.init totalBytesToTransfer in
   let calcProgress i bytes dbg =
     let i = Uutil.File.toLine i in
     let item = items.(i) in
     item.bytesTransferred <- Uutil.Filesize.add item.bytesTransferred bytes;
     totalBytesTransferred := Uutil.Filesize.add !totalBytesTransferred bytes;
-    let v =
-      (Uutil.Filesize.percentageOfTotalSize
-         !totalBytesTransferred !totalBytesToTransfer)
-    in
+    let totalTransferred =
+      Uutil.Filesize.(add !totalBytesTransferred (ofInt !totalItemsTransferred)) in
+    Uutil.Filesize.percentageOfTotalSize totalTransferred totalToTransfer
+  in
+  let tlog = ref (Unix.gettimeofday ()) in
+  let t = ref 0. in
+  let prevItems = ref 0 in
+  let displayProgress v =
     let t1 = Unix.gettimeofday () in
-    let remTime =
-      if v <= 0. then "--:--"
-      else if v >= 100. then "00:00"
-      else
-        let t = truncate ((t1 -. t0) *. (100. -. v) /. v +. 0.5) in
-        Format.sprintf "%02d:%02d" (t / 60) (t mod 60)
-    in
-    t1, Format.sprintf "%s  %s ETA" (Util.percent2string v) remTime
+    let () = Uicommon.Stats.update sta t1 !totalBytesTransferred in
+    if t1 -. !t >= 0.1 || !prevItems <> !totalItemsTransferred then begin
+      t := t1;
+      prevItems := !totalItemsTransferred;
+      let remTime =
+        if v <= 0. then "--:--"
+        else if v >= 100. then "00:00:00"
+        else
+          let rate = Uicommon.Stats.avgRate1 sta in
+          if Float.is_nan rate then "--:--"
+          else
+            Format.sprintf "%8s/s    %s"
+              (Util.bytes2string (Int64.of_float rate))
+              (Uicommon.Stats.eta sta "--:--")
+      in
+      let totalBytesTransferredStr = Util.bytes2string
+        (Uutil.Filesize.toInt64 !totalBytesTransferred) in
+      let s = Format.sprintf "%s  %d/%s  (%s of %s)  %s ETA"
+        (Util.percent2string v)
+        !totalItemsTransferred totalItemsToTransferStr
+        totalBytesTransferredStr totalBytesToTransferStr remTime in
+
+      if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
+        Util.set_infos s;
+      if (Prefs.read Trace.terse) || (Prefs.read Globals.batch) then
+        if (t1 -. !tlog) >= 60. then
+        begin
+          Trace.logonly (s ^ "\n");
+          tlog := t1
+        end
+    end
   in
-  let tlog = ref t0 in
   let showProgress i bytes dbg =
-    let t1, s = calcProgress i bytes dbg in
-    if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
-      Util.set_infos s;
-    if (Prefs.read Trace.terse) || (Prefs.read Globals.batch) then
-      if (t1 -. !tlog) >= 60. then
-      begin
-        Trace.logonly (s ^ "\n");
-        tlog := t1
-      end
+    let v = calcProgress i bytes dbg in
+    displayProgress v
   in
   Uutil.setProgressPrinter showProgress;
 
-  Transport.logStart ();
+  let intrcount = ref 0 in
+  let sigtermHandler _ =
+    if !intrcount >= 3 then raise Sys.Break;
+    Abort.all ();
+    incr intrcount
+  in
+  let ctrlCHandler n =
+    sigtermHandler n;
+    if !intrcount = 1 then
+      let s = "\n\nUpdate propagation interrupted. It may take a while \
+        to stop.\nIf the process doesn't stop soon then wait or press \
+        Ctrl-C\n3 more times to force immediate termination.\n\n\n" in
+      (* Don't use [Printf.*printf] or [Format.*printf] (or other functions
+         which use [Stdlib.out_channel]) because this can cause a deadlock
+         with other outputting functions (in this case most likely at
+         [Util.set_infos] called in [showProgress]) before OCaml 4.12. *)
+      try Unix.write_substring Unix.stdout s 0 (String.length s) |> ignore
+      with Unix.Unix_error _ -> ()
+  in
+  let stopAtIntr f =
+    let signal_noerr signa behv =
+      try Some (Sys.signal signa behv)
+      with Sys_error _ | Invalid_argument _ -> None
+    in
+    let restore_noerr signa = function
+    | Some prevSig -> ignore (signal_noerr signa prevSig)
+    | None -> ()
+    in
+    let prevSigInt = signal_noerr Sys.sigint (Signal_handle ctrlCHandler) in
+    let prevSigTerm = signal_noerr Sys.sigterm (Signal_handle sigtermHandler) in
+    let restoreSig () =
+      (* Set handlers will still raise [Sys.Break]; can ignore errors here. *)
+      restore_noerr Sys.sigint prevSigInt;
+      restore_noerr Sys.sigterm prevSigTerm
+    in
+
+    try f (); restoreSig ()
+    with e ->
+      let origbt = Printexc.get_raw_backtrace () in
+      restoreSig ();
+      Printexc.raise_with_backtrace e origbt
+  in
+
+  if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
+    Util.set_infos "Starting...";
+  Uicommon.transportStart ();
   let fFailedPaths = ref [] in
   let fPartialPaths = ref [] in
-  let uiWrapper i item f =
-    Lwt.try_bind f
+  let notstarted = ref (Array.length items) in
+  let progressItem i =
+    incr totalItemsTransferred;
+    showProgress (Uutil.File.ofLine i) Uutil.Filesize.zero "itm"
+  in
+  let uiWrapper i item =
+    Lwt.try_bind
+      (fun () -> decr notstarted;
+                 Transport.transportItem item.ri
+                   (Uutil.File.ofLine i) verifyMerge)
       (fun () ->
-         if partiallyProblematic item.ri && not (problematic item.ri) then
+         let notSkip = not (isSkip item.ri) in
+         if partiallyProblematic item.ri && notSkip then
            fPartialPaths := item.ri.path1 :: !fPartialPaths;
+         if notSkip then progressItem i;
          Lwt.return ())
       (fun e ->
+        if not (isSkip item.ri) then progressItem i;
         match e with
           Util.Transient s ->
             let rem =
@@ -856,42 +943,20 @@ let doTransport reconItemList =
             return ()
         | _ ->
             fail e) in
-  let im = Array.length items in
-  let rec loop i actions pRiThisRound =
-    if i < im then begin
-      let item = items.(i) in
-      let actions =
-        if pRiThisRound item.ri then
-          uiWrapper i item
-            (fun () -> Transport.transportItem item.ri
-                         (Uutil.File.ofLine i) verifyMerge)
-          :: actions
-        else
-          actions
-      in
-      loop (i + 1) actions pRiThisRound
-    end else
-      actions
-  in
-  Lwt_unix.run
-    (let actions = loop 0 [] (fun ri -> not (Common.isDeletion ri)) in
-     Lwt_util.join actions);
-  Lwt_unix.run
-    (let actions = loop 0 [] Common.isDeletion in
-     Lwt_util.join actions);
-  Transport.logFinish ();
+  stopAtIntr begin fun () ->
+    Uicommon.transportItems items (fun {ri; _} -> not (Common.isDeletion ri)) uiWrapper;
+    Uicommon.transportItems items (fun {ri; _} -> Common.isDeletion ri) uiWrapper
+  end;
+  Uicommon.transportFinish ();
 
   Uutil.setProgressPrinter (fun _ _ _ -> ());
   Util.set_infos "";
 
-  (Safelist.rev !fFailedPaths, Safelist.rev !fPartialPaths)
+  (Safelist.rev !fFailedPaths, Safelist.rev !fPartialPaths, !notstarted, !intrcount > 0)
 
 let setWarnPrinterForInitialization()=
   Util.warnPrinter :=
-     Some(fun s ->
-            alwaysDisplay "Error: ";
-            alwaysDisplay (s^"\n");
-            exit Uicommon.fatalExit)
+    Some (fun s -> alwaysDisplay ("Warning: " ^ s ^ "\n\n"))
 
 let setWarnPrinter() =
   Util.warnPrinter :=
@@ -925,30 +990,76 @@ let formatStatus major minor =
     s
 
 let rec interactAndPropagateChanges prevItemList reconItemList
-            : bool * bool * bool * (Path.t list)
-              (* anySkipped?, anyPartial?, anyFailures?, failingPaths *) =
+      : bool * bool * bool * bool * (Path.t list)
+        (* anySkipped?, anyPartial?, anyFailures?, anyCancels?, failingPaths *) =
   let (proceed,newReconItemList) = interact prevItemList reconItemList in
-  let (updatesToDo, skipped) =
+  let isSkip = problematic in
+  let (updatesToDo, skipped, (totalBytesToRoot1, totalBytesToRoot2)) =
     Safelist.fold_left
-      (fun (howmany, skipped) ri ->
-        if problematic ri then (howmany, skipped + 1)
-        else (howmany + 1, skipped))
-      (0, 0) newReconItemList in
+      (fun (howmany, skipped, (bytes1, bytes2)) ri ->
+        if isSkip ri then (howmany, skipped + 1, (bytes1, bytes2))
+        else (howmany + 1, skipped,
+          match ri.replicas with
+          | Problem _ -> (bytes1, bytes2)
+          | Different {direction; _} ->
+              match direction with
+              | Conflict _ | Merge -> (bytes1, bytes2)
+              | Replica1ToReplica2 -> (bytes1, Uutil.Filesize.add (Common.riLength ri) bytes2)
+              | Replica2ToReplica1 -> (Uutil.Filesize.add (Common.riLength ri) bytes1, bytes2)))
+      (0, 0, (Uutil.Filesize.zero, Uutil.Filesize.zero)) newReconItemList in
+  if not (Prefs.read Trace.terse) && (updatesToDo > 0 || skipped > 0) then begin
+    let root1, root2 =
+      match Globals.roots () with
+      | (Local, path1), (Local, path2) -> Fspath.differentSuffix path1 path2
+      | (Local, _), (Remote host, _) -> "local", host
+      | (Remote host, _), (Local, _) -> host, "local"
+      | (Remote host1, _), (Remote host2, _) -> host1, host2
+    in
+    Trace.log_color (Printf.sprintf
+      "\n%s%d%s items will be synced, %s%d%s skipped\n\
+       %s to be synced from %s to %s\n\
+       %s to be synced from %s to %s\n"
+       (color `Focus) updatesToDo (color `Reset)
+       (color `Information) skipped (color `Reset)
+       (Util.bytes2string (Uutil.Filesize.toInt64 totalBytesToRoot2)) root1 root2
+       (Util.bytes2string (Uutil.Filesize.toInt64 totalBytesToRoot1)) root2 root1)
+  end;
+  let doTransp () =
+    try
+      doTransport newReconItemList skipped isSkip
+    with e ->
+      let origbt = Printexc.get_raw_backtrace () in
+      let summary =
+        "\nSynchronization "
+          ^ (color `Failure)
+          ^ (match e with Sys.Break -> "interrupted" | _ -> "failed")
+          ^ (color `Reset)
+          ^ (try let tm = Util.localtime (Util.time ()) in
+             Printf.sprintf " at %02d:%02d:%02d"
+               tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec with _ -> "")
+          ^ (match e with Sys.Break -> " by user request" | _ -> " due to a fatal error")
+          ^ "\n\n"
+      in
+      Util.set_infos "";
+      Trace.log_color summary;
+      Printexc.raise_with_backtrace e origbt
+  in
   let doit() =
     if not (Prefs.read Globals.batch || Prefs.read Trace.terse) then newLine();
     if not (Prefs.read Trace.terse) then Trace.status "Propagating updates";
     let timer = Trace.startTimer "Transmitting all files" in
-    let (failedPaths, partialPaths) = doTransport newReconItemList in
+    let (failedPaths, partialPaths, notstarted, intr) = doTransp () in
     let failures = Safelist.length failedPaths in
     let partials = Safelist.length partialPaths in
     Trace.showTimer timer;
     if not (Prefs.read Trace.terse) then Trace.status "Saving synchronizer state";
     Update.commitUpdates ();
-    let trans = updatesToDo - failures in
+    let trans = updatesToDo - notstarted - failures in
     let summary =
       Printf.sprintf
-       "Synchronization %s at %s  (%d item%s transferred, %s%s, %s)"
-       (if failures = 0 then (color `Success) ^ "complete" ^ (color `Reset) else (color `Failure) ^ "incomplete" ^ (color `Reset))
+       "Synchronization %s at %s  (%d item%s transferred, %s%s, %s%s)"
+       (if failures = 0 && notstarted = 0 then (color `Success) ^ "complete" ^ (color `Reset)
+        else (color `Failure) ^ "incomplete" ^ (color `Reset))
        (let tm = Util.localtime (Util.time()) in
         Printf.sprintf "%02d:%02d:%02d"
           tm.Unix.tm_hour tm.Unix.tm_min tm.Unix.tm_sec)
@@ -958,7 +1069,8 @@ let rec interactAndPropagateChanges prevItemList reconItemList
         else
           "")
        (if skipped = 0 then "0 skipped" else (color `Information) ^ (Printf.sprintf "%d skipped" skipped) ^ (color `Reset))
-       (if failures = 0 then "0 failed" else (color `Failure) ^ (Printf.sprintf "%d failed" failures) ^ (color `Reset)) in
+       (if failures = 0 then "0 failed" else (color `Failure) ^ (Printf.sprintf "%d failed" failures) ^ (color `Reset))
+       (if notstarted = 0 then "" else ", " ^ (color `Information) ^ (Printf.sprintf "%d not started" notstarted) ^ (color `Reset)) in
     Trace.log_color (summary ^ "\n");
     if skipped>0 then
       Safelist.iter
@@ -979,7 +1091,8 @@ let rec interactAndPropagateChanges prevItemList reconItemList
       Safelist.iter
         (fun p -> alwaysDisplayAndLog ("  failed: " ^ (Path.toString p)))
         failedPaths;
-    (skipped > 0, partials > 0, failures > 0, failedPaths) in
+    if intr then raise Sys.Break; (* Make sure repeat mode is stopped *)
+    (skipped > 0, partials > 0, failures > 0, notstarted > 0, failedPaths) in
   if updatesToDo = 0 then begin
     (* BCP (3/09): We need to commit the archives even if there are
        no updates to propagate because some files (in fact, if we've
@@ -988,7 +1101,7 @@ let rec interactAndPropagateChanges prevItemList reconItemList
     (* JV (5/09): Don't save the archive in repeat mode as it has some
        costs and its unlikely there is much change to the archives in
        this mode. *)
-    if !Update.foundArchives && Prefs.read Uicommon.repeat = "" then
+    if !Update.foundArchives && Prefs.read Uicommon.repeat = `NoRepeat then
       Update.commitUpdates ();
     display "No updates to propagate\n";
     if skipped > 0 then begin
@@ -1014,7 +1127,7 @@ let rec interactAndPropagateChanges prevItemList reconItemList
          | _ -> ())
         newReconItemList
       end;
-    (skipped > 0, false, false, [])
+    (skipped > 0, false, false, false, [])
   end else if proceed=ProceedImmediately then begin
     doit()
   end else
@@ -1096,24 +1209,46 @@ let checkForDangerousPath dangerousPaths =
     end
   end
 
-let synchronizeOnce ?wantWatcher ?skipRecentFiles pathsOpt =
+let displayWaitMessage () =
+  if not (Prefs.read silent) then
+    Util.msg "%s\n" (Uicommon.contactingServerMsg ())
+
+(* Most modern VT100 terminal emulators (and some ANSI) are able to switch
+   automatic line-wrapping off and on by control sequences ESC[?7l and ESC[?7h.
+   This here is a very blunt heuristic to filter out some that can't do it or
+   use a different control sequence. It does not need to be exact, as long as
+   it covers the vast majority of supported systems. *)
+let termNowrapOk =
+  System.termVtCapable Unix.stdout &&
+  let s = try System.getenv "TERM" with Not_found -> "" in
+  not (
+    s = "dumb"
+    || s = "emacs"
+    || Util.startswith s "sun"
+    || Util.startswith s "cons"
+    || Util.startswith s "eterm"
+    || Util.startswith s "cygwin"
+    || Util.startswith s "dvtm"
+  )
+
+let synchronizeOnce ?wantWatcher pathsOpt =
   let showStatus path =
     if path = "" then Util.set_infos "" else
-    let max_len = 70 in
-    let mid = (max_len - 3) / 2 in
-    let path =
+    let shorten path =
+      let max_len = 70 in
+      let mid = (max_len - 3) / 2 in
       let l = String.length path in
       if l <= max_len then path else
       String.sub path 0 (max_len - mid - 3) ^ "..." ^
       String.sub path (l - mid) mid
     in
     let c = "-\\|/".[truncate (mod_float (4. *. Unix.gettimeofday ()) 4.)] in
-    Util.set_infos (Format.sprintf "%c %s" c path)
+    if termNowrapOk && not (Prefs.read dumbtty) then
+      Util.set_infos (Format.sprintf "%c \027[?7l%s\027[?7h" c path) ~clr:"\r\027[K\r"
+    else
+      Util.set_infos (Format.sprintf "%c %s" c (shorten path))
   in
-  Uicommon.refreshConnection
-    ~displayWaitMessage:(fun () -> if not (Prefs.read silent)
-                         then Util.msg "%s\n" (Uicommon.contactingServerMsg()))
-    ~termInteract:None;
+  Uicommon.connectRoots ~displayWaitMessage ();
   Trace.status "Looking for changes";
   if not (Prefs.read Trace.terse) && (Prefs.read Trace.debugmods = []) then
     Uutil.setUpdateStatusPrinter (Some showStatus);
@@ -1131,7 +1266,7 @@ let synchronizeOnce ?wantWatcher ?skipRecentFiles pathsOpt =
 
   if not !Update.foundArchives then Update.commitUpdates ();
   if reconItemList = [] then begin
-    if !Update.foundArchives && Prefs.read Uicommon.repeat = "" then
+    if !Update.foundArchives && Prefs.read Uicommon.repeat = `NoRepeat then
       Update.commitUpdates ();
     (if anyEqualUpdates then
       Trace.status ("Nothing to do: replicas have been changed only "
@@ -1141,12 +1276,85 @@ let synchronizeOnce ?wantWatcher ?skipRecentFiles pathsOpt =
     (Uicommon.perfectExit, [])
   end else begin
     checkForDangerousPath dangerousPaths;
-    let (anySkipped, anyPartial, anyFailures, failedPaths) =
+    let (anySkipped, anyPartial, anyFailures, anyCancel, failedPaths) =
       interactAndPropagateChanges [] reconItemList in
-    let exitStatus = Uicommon.exitCode(anySkipped || anyPartial,anyFailures) in
+    let exitStatus = Uicommon.exitCode (anySkipped || anyPartial || anyCancel, anyFailures) in
     (exitStatus, failedPaths)
   end
 
+(* ------------ Safe termination between synchronizations ------------ *)
+
+let safeStopReqd, requestSafeStop =
+  let safeStopReqd = ref false in
+  (* [safeStopReqd] can only go from false to true;
+     it must never be changed from true to false. *)
+  let isRequested () = !safeStopReqd
+  and request () = safeStopReqd := true in
+  isRequested, request
+
+(*** Requesting safe termination by signals ***)
+
+let set_signal_noerr signa nm behv =
+  try Sys.set_signal signa behv; true
+  with Invalid_argument _ | Sys_error _ as e ->
+    Trace.logonly
+      ("Warning: " ^ nm ^ " handler not set: " ^ (Printexc.to_string e) ^ "\n");
+    false
+
+let stopPipe = ref None
+
+let setupSafeStop () =
+  if supportSignals then begin
+    let safeStop _ =
+      if not (safeStopReqd ()) then begin
+        requestSafeStop ();
+        (* Interrupt the interruptible sleep *)
+        match !stopPipe with
+        | Some (i, o) -> Unix.close o; Lwt_unix.close i
+        | None -> ()
+      end
+    in
+    Util.blockSignals [Sys.sigusr2] (fun () ->
+    let ok = set_signal_noerr Sys.sigusr2 "SIGUSR2" (Signal_handle safeStop) in
+    if ok then stopPipe := Some (Lwt_unix.pipe_in ~cloexec:true ()))
+  end
+
+let safeStopRequested () =
+  safeStopReqd ()
+
+(*** Sleep interruptible by a termination request ***)
+
+let safeStopWait =
+  let safeStopWait_aux () =
+    let readStop =
+      match !stopPipe with
+      | None -> Lwt.wait ()
+      | Some (i, _) -> Lwt_unix.wait_read i
+    in
+    let readFail = function
+      | Unix.Unix_error (EBADF, _, _) -> Lwt.return (requestSafeStop ())
+      | e -> Lwt.fail e
+    in
+    let rec loop () =
+      Lwt.catch
+        (fun () -> readStop) readFail >>= fun () ->
+      if not (safeStopRequested ()) then
+        Lwt_unix.sleep 0.15 >>= loop
+      else
+        Lwt.return ()
+    in
+    loop ()
+  in
+  let wt = ref None in
+  fun () ->
+    match !wt with
+    | Some t -> t
+    | None -> let t = safeStopWait_aux () in wt := Some t; t
+
+let interruptibleSleepf dt =
+  Lwt_unix.run (Lwt.choose [Lwt_unix.sleep dt; safeStopWait ()])
+let interruptibleSleep dt = interruptibleSleepf (float dt)
+
 (* ----------------- Filesystem watching mode ---------------- *)
 
 let watchinterval = 1.    (* Minimal interval between two synchronizations *)
@@ -1157,7 +1365,7 @@ module PathMap = Map.Make (Path)
 
 let waitForChangesRoot: Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "waitForChanges"
+    "waitForChanges" Umarshal.unit Umarshal.unit
     (fun (fspath, _) -> Fswatchold.wait (Update.archiveHash fspath))
 
 let waitForChanges t =
@@ -1167,12 +1375,17 @@ let waitForChanges t =
     Lwt_unix.run
       (Globals.allRootsMap (fun r -> Lwt.return (waitForChangesRoot r ()))
          >>= fun l ->
-       Lwt.choose (timeout @ l))
+       Lwt.choose (timeout @ l @ [safeStopWait ()]))
   end
 
-let synchronizePathsFromFilesystemWatcher () =
-  let rec loop isStart delayInfo =
+let synchronizePathsFromFilesystemWatcher fullintv =
+  let fullinterval = match fullintv with None -> 1e20 | Some i -> float i in
+  let rec loop lastFull delayInfo =
     let t = Unix.gettimeofday () in
+    let sinceFull = t -. lastFull in
+    let isFull = sinceFull > fullinterval in
+    let lastFull = if isFull then t else lastFull in
+    let nextFull = lastFull +. fullinterval in
     let (delayedPaths, readyPaths) =
       PathMap.fold
         (fun p (t', _) (delayed, ready) ->
@@ -1180,8 +1393,8 @@ let synchronizePathsFromFilesystemWatcher () =
         delayInfo ([], [])
     in
     let (exitStatus, failedPaths) =
-      synchronizeOnce ~wantWatcher:() ~skipRecentFiles:()
-        (if isStart then None else Some (readyPaths, delayedPaths))
+      synchronizeOnce ~wantWatcher:true
+        (if isFull then None else Some (readyPaths, delayedPaths))
     in
     (* After a failure, we retry at once, then use an exponential backoff *)
     let delayInfo =
@@ -1199,57 +1412,56 @@ let synchronizePathsFromFilesystemWatcher () =
         PathMap.empty
         (Safelist.append delayedPaths failedPaths)
     in
-    Lwt_unix.run (Lwt_unix.sleep watchinterval);
+    interruptibleSleepf watchinterval;
     let nextTime =
-      PathMap.fold (fun _ (t, d) t' -> min t t') delayInfo 1e20 in
-    waitForChanges nextTime;
-    loop false delayInfo
+      PathMap.fold (fun _ (t, d) t' -> min t t') delayInfo nextFull in
+    if not (safeStopRequested ()) then waitForChanges nextTime;
+    if safeStopRequested () then exitStatus else loop lastFull delayInfo
   in
-  loop true PathMap.empty
+  loop 0. PathMap.empty
 
 (* ----------------- Repetition ---------------- *)
 
 let synchronizeUntilNoFailures repeatMode =
+  let wantWatcher = repeatMode in
   let rec loop triesLeft pathsOpt =
     let (exitStatus, failedPaths) =
-      synchronizeOnce
-        ?wantWatcher:(if repeatMode then Some () else None) pathsOpt in
-    if failedPaths <> [] && triesLeft <> 0 then begin
+      synchronizeOnce ~wantWatcher pathsOpt in
+    if failedPaths <> [] && triesLeft <> 0
+         && not (repeatMode && safeStopRequested ()) then begin
       loop (triesLeft - 1) (Some (failedPaths, []))
     end else begin
       exitStatus
     end in
   loop (Prefs.read Uicommon.retry) None
 
-let rec synchronizeUntilDone () =
-  let repeatinterval =
-    if Prefs.read Uicommon.repeat = "" then -1 else
-    try int_of_string (Prefs.read Uicommon.repeat)
-    with Failure _ ->
-      (* If the 'repeat' pref is not a valid number, switch modes... *)
-      if Prefs.read Uicommon.repeat = "watch" then
-        synchronizePathsFromFilesystemWatcher()
-      else
-        raise (Util.Fatal ("Value of 'repeat' preference ("
-                           ^Prefs.read Uicommon.repeat
-                           ^") should be either a number or 'watch'\n")) in
-
+let rec synchronizeUntilDone repeatinterval =
   let exitStatus = synchronizeUntilNoFailures(repeatinterval >= 0) in
-  if repeatinterval < 0 then
+  if repeatinterval < 0 || safeStopRequested () then
     exitStatus
   else begin
     (* Do it again *)
     Trace.status (Printf.sprintf
        "\nSleeping for %d seconds...\n" repeatinterval);
-    Unix.sleep repeatinterval;
-    synchronizeUntilDone ()
+    interruptibleSleep repeatinterval;
+    if safeStopRequested () then exitStatus else synchronizeUntilDone repeatinterval
   end
 
+let synchronizeUntilDone () =
+  match Prefs.read Uicommon.repeat with
+  | `Watch -> synchronizePathsFromFilesystemWatcher None
+  | `WatchAndInterval i -> synchronizePathsFromFilesystemWatcher (Some i)
+  | `Interval i -> synchronizeUntilDone i
+  | `NoRepeat -> synchronizeUntilDone (-1)
+  | `Invalid (_, e) -> raise e
+
 (* ----------------- Startup ---------------- *)
 
 let profmgrPrefName = "i"
 let profmgrPref =
-  Prefs.createBool profmgrPrefName false ~local:true
+  Prefs.createBool profmgrPrefName false
+    ~category:(`Basic `CLI)
+    ~cli_only:true
     "interactive profile mode (text UI); command-line only"
     ("Provide this preference in the command line arguments to enable "
      ^ "interactive profile manager in the text user interface. Currently "
@@ -1284,18 +1496,26 @@ let addProfileKeys list default =
   in
   addKey 0 [] list
 
+let scanProfiles () =
+  let wp = !Util.warnPrinter in
+  (* Replace warn printer with something that doesn't quit
+     the UI just for errors in random scanned profiles. *)
+  Util.warnPrinter := Some (fun s -> alwaysDisplay ("Warning: " ^ s ^ "\n\n"));
+  let () = Uicommon.scanProfiles () in
+  Util.warnPrinter := wp
+
 let getProfile default =
   let cmdArgs = Prefs.scanCmdLine Uicommon.shortUsageMsg in
-  Uicommon.scanProfiles ();
   if Util.StringMap.mem Uicommon.runTestsPrefName cmdArgs ||
     not (Util.StringMap.mem profmgrPrefName cmdArgs) then
     Some default
   else
+  let () = scanProfiles () in
   if (List.length !Uicommon.profilesAndRoots) > 10 then begin
     Trace.log (Format.sprintf "You have too many profiles in %s \
                 for interactive selection. Please specify profile \
                 or roots on command line.\n"
-                (System.fspathToPrintString Util.unisonDir));
+                Util.unisonDir);
     Trace.log "The profile names are:\n";
     Safelist.iter (fun (p, _) -> Trace.log (Format.sprintf "  %s\n" p))
       !Uicommon.profilesAndRoots;
@@ -1372,27 +1592,48 @@ let rec start interface =
   if interface <> Uicommon.Text then
     Util.msg "This Unison binary only provides the text GUI...\n";
   begin try
+    Sys.catch_break true;
     (* Just to make sure something is there... *)
     setWarnPrinterForInitialization();
-    Uicommon.uiInit
-      ~reportError:
-      (fun s -> Util.msg "%s%s\n\n%s\n" Uicommon.shortUsageMsg profmgrUsageMsg s; exit 1)
-      ~tryAgainOrQuit:
-      (fun s -> Util.msg "%s" Uicommon.shortUsageMsg; exit 1)
-      ~displayWaitMessage:
-      (fun () -> setWarnPrinter();
-                 if Prefs.read silent then Prefs.set Trace.terse true;
-                 if not (Prefs.read silent)
-                 then Util.msg "%s\n" (Uicommon.contactingServerMsg()))
-      ~getProfile:
-      (fun () -> let prof = getProfile "default" in restoreTerminal(); prof)
-      ~getFirstRoot:
-      (fun () -> Util.msg "%s%s\n" Uicommon.shortUsageMsg profmgrUsageMsg; exit 1)
-      ~getSecondRoot:
-      (fun () -> Util.msg "%s%s\n" Uicommon.shortUsageMsg profmgrUsageMsg; exit 1)
-      ~termInteract:
-      None
-      ();
+    setupSafeStop ();
+    let errorOut s =
+      Util.msg "%s%s%s\n" Uicommon.shortUsageMsg profmgrUsageMsg s;
+      exit 1
+    in
+    let profileName = match Uicommon.uiInitClRootsAndProfile () with
+      | Error s -> errorOut ("\n\n" ^ s)
+      | Ok None ->
+          let profile = getProfile "default" in
+          let () = restoreTerminal () in
+          begin
+            match profile with
+            | None -> exit 0
+            | Some x -> x
+          end
+      | Ok (Some s) -> s
+    in
+    Uicommon.initPrefs ~profileName ~promptForRoots:(fun () -> errorOut "") ()
+  with e ->
+    handleException e;
+    exit Uicommon.fatalExit
+  end;
+
+  (* Uncaught exceptions up to this point are non-recoverable, treated
+     as permanent and will inevitably exit the process. Uncaught exceptions
+     from here onwards are treated as potentially temporary or recoverable.
+     The process does not have to exit if in repeat mode and can try again. *)
+  begin try
+    if Prefs.read silent then Prefs.set Trace.terse true;
+
+    Uicommon.connectRoots ~displayWaitMessage ();
+
+    if Prefs.read Uicommon.testServer then exit 0;
+
+    (* Run unit tests if requested *)
+    if Prefs.read Uicommon.runtests then begin
+      !Uicommon.testFunction ();
+      exit 0
+    end;
 
     (* Some preference settings imply others... *)
     if Prefs.read silent then begin
@@ -1401,7 +1642,7 @@ let rec start interface =
       Prefs.set dumbtty true;
       Trace.sendLogMsgsToStderr := false;
     end;
-    if Prefs.read Uicommon.repeat <> "" then begin
+    if Prefs.read Uicommon.repeat <> `NoRepeat then begin
       Prefs.set Globals.batch true;
     end;
     setColorPreference ();
@@ -1426,23 +1667,38 @@ let rec start interface =
       handleException Sys.Break;
       exit Uicommon.fatalExit
     end
+  | e when breakRepeat e -> begin
+      handleException e;
+      exit Uicommon.fatalExit
+    end
   | e -> begin
       (* If any other bad thing happened and the -repeat preference is
          set, then restart *)
-      (* JV: it seems safer to just abort here, as we don't know in which
-         state Unison is; for instance, if the connection is lost, there
-         is no point in restarting as Unison will currently not attempt to
-         establish a new connection. *)
       handleException e;
-      if false (*Prefs.read Uicommon.repeat <> ""*) then begin
-        Util.msg "Restarting in 10 seconds...\n";
-        Unix.sleep 10;
-        start interface
-      end else
-        exit Uicommon.fatalExit
+      if Prefs.read Uicommon.repeat = `NoRepeat
+          || Prefs.read Uicommon.runtests then
+        exit Uicommon.fatalExit;
+
+      Util.msg "\nRestarting in 10 seconds...\n\n";
+      begin try interruptibleSleep 10 with Sys.Break -> exit Uicommon.fatalExit end;
+      if safeStopRequested () then exit Uicommon.fatalExit else start interface
     end
   end
 
+(* Though in some cases we could, there's no point in recovering
+   and continuing at any of these exceptions. *)
+and breakRepeat = function
+  (* Programming errors *)
+  | Assert_failure _
+  | Match_failure _
+  | Invalid_argument _
+  | Fun.Finally_raised _
+  (* Async exceptions *)
+  | Out_of_memory
+  | Stack_overflow
+  | Sys.Break -> true
+  | _ -> false
+
 let defaultUi = Uicommon.Text
 
 end
diff --git a/src/unicode.mli b/src/unicode.mli
index 2d82a6e..6112a1b 100644
--- a/src/unicode.mli
+++ b/src/unicode.mli
@@ -36,7 +36,7 @@ val from_utf_16 : string -> string
 val to_utf_16_filename : string -> string
 val from_utf_16_filename : string -> string
 
-(* Check wether the string contains only well-formed UTF-8 characters *)
+(* Check whether the string contains only well-formed UTF-8 characters *)
 val check_utf_8 : string -> bool
 
 (* Convert a string to UTF-8 by keeping all UTF-8 characters unchanged
diff --git a/src/update.ml b/src/update.ml
index a77b710..3548603 100644
--- a/src/update.ml
+++ b/src/update.ml
@@ -25,7 +25,8 @@ let debugignore = Trace.debug "ignore"
 
 let ignoreArchives =
   Prefs.createBool "ignorearchives" false
-    "!ignore existing archive files"
+    ~category:(`Advanced `Archive)
+    "ignore existing archive files"
     ("When this preference is set, Unison will ignore any existing "
      ^ "archive files and behave as though it were being run for the first "
      ^ "time on these replicas.  It is "
@@ -40,31 +41,68 @@ let ignoreArchives =
    archive changes: old archives will then automatically be discarded.  (We
    do not use the unison version number for this because usually the archive
    representation does not change between unison versions.) *)
-(*FIX: also change Fileinfo.stamp to drop the info.ctime component, next
-  time the format is modified *)
-(*FIX: also make Jerome's suggested change about file times (see his mesg in
-       unison-pending email folder). *)
-(*FIX: we could also drop the use of 8.3-style filenames on Windows, next
-  time the format is changed *)
-(* FIX: use a special stamp rather than the current hack to leave a flag
-   in the archive when a file transfer fails so as to turn off fastcheck
-   for this file on the next sync. *)
 (*FIX: consider changing the way case-sensitivity mode is stored in
   the archive *)
-(*FIX: we should use only one Marshal.from_channel *)
-let archiveFormat = 22
+let archiveFormat = 23
 
 module NameMap = MyMap.Make (Name)
 
+(* IMPORTANT!
+   This is the 2.51-compatible version of type [archive]. It must always remain
+   exactly the same as the type [archive] in version 2.51.5. This means that if
+   any of the types it is composed of changes, for each changed type a 2.51-
+   compatible version must be created (like has been done for [Props.t]). *)
+type archive251 =
+    ArchiveDir of Props.t251 * archive251 NameMap.t
+  | ArchiveFile of Props.t251 * Os.fullfingerprint * Fileinfo.stamp251 * Osx.ressStamp
+  | ArchiveSymlink of string
+  | NoArchive
+
 type archive =
     ArchiveDir of Props.t * archive NameMap.t
   | ArchiveFile of Props.t * Os.fullfingerprint * Fileinfo.stamp * Osx.ressStamp
   | ArchiveSymlink of string
   | NoArchive
 
+let marchive_rec marchive =
+  Umarshal.(sum4
+              (prod2 Props.m (NameMap.m marchive) id id)
+              (prod4 Props.m Os.mfullfingerprint Fileinfo.mstamp Osx.mressStamp id id)
+              string unit
+              (function
+               | ArchiveDir (a, b) -> I41 (a, b)
+               | ArchiveFile (a, b, c, d) -> I42 (a, b, c, d)
+               | ArchiveSymlink a -> I43 a
+               | NoArchive -> I44 ())
+              (function
+               | I41 (a, b) -> ArchiveDir (a, b)
+               | I42 (a, b, c, d) -> ArchiveFile (a, b, c, d)
+               | I43 a -> ArchiveSymlink a
+               | I44 () -> NoArchive))
+
+let marchive = Umarshal.rec1 marchive_rec
+
 (* For directories, only the permissions part of the file description (desc)
    is used for synchronization at the moment. *)
 
+let rec to_compat251 (arch : archive) : archive251 =
+  match arch with
+  | ArchiveDir (desc, children) ->
+      ArchiveDir (Props.to_compat251 desc, NameMap.map to_compat251 children)
+  | ArchiveFile (desc, dig, stamp, ress) ->
+      ArchiveFile (Props.to_compat251 desc, dig, Fileinfo.stamp_to_compat251 stamp, ress)
+  | ArchiveSymlink content -> ArchiveSymlink content
+  | NoArchive -> NoArchive
+
+let rec of_compat251 (arch : archive251) : archive =
+  match arch with
+  | ArchiveDir (desc, children) ->
+      ArchiveDir (Props.of_compat251 desc, NameMap.map of_compat251 children)
+  | ArchiveFile (desc, dig, stamp, ress) ->
+      ArchiveFile (Props.of_compat251 desc, dig, Fileinfo.stamp_of_compat251 stamp, ress)
+  | ArchiveSymlink content -> ArchiveSymlink content
+  | NoArchive -> NoArchive
+
 let archive2string = function
     ArchiveDir(_) -> "ArchiveDir"
   | ArchiveFile(_) -> "ArchiveFile"
@@ -105,7 +143,9 @@ let archive2string = function
    reasons. *)
 
 let rootsName : string Prefs.t =
-  Prefs.createString "rootsName" "" "*Canonical root names" ""
+  Prefs.createString "rootsName" ""
+    ~category:(`Internal `Pseudo)
+    "*Canonical root names" ""
 
 let getRootsName () = Prefs.read rootsName
 
@@ -117,7 +157,8 @@ let foundArchives = ref true
 
 let rootAliases : string list Prefs.t =
   Prefs.createStringList "rootalias"
-   "!register alias for canonical root names"
+    ~category:(`Advanced `General)
+   "register alias for canonical root names"
    ("When calculating the name of the archive files for a given pair of roots,"
    ^ " Unison replaces any roots matching the left-hand side of any rootalias"
    ^ " rule by the corresponding right-hand side.")
@@ -156,15 +197,6 @@ let storeRootsName () =
                (Globals.rootsInCanonicalOrder())))) in
   Prefs.set rootsName n
 
-(* How many characters of the filename should be used for the unique id of
-   the archive?  On Unix systems, we use the full fingerprint (32 bytes).
-   On windows systems, filenames longer than 8 bytes can cause problems, so
-   we chop off all but the first 6 from the fingerprint. *)
-let significantDigits =
-  match Util.osType with
-    `Win32 -> 6
-  | `Unix -> 32
-
 let thisRootsGlobalName (fspath: Fspath.t): string =
   root2stringOrAlias (Common.Remote (Os.myCanonicalHostName ()), fspath)
 
@@ -173,11 +205,26 @@ let thisRootsGlobalName (fspath: Fspath.t): string =
 (* The status of an archive *)
 type archiveVersion = MainArch | NewArch | ScratchArch | Lock | FPCache
 
+let marchiveVersion = Umarshal.(sum5 unit unit unit unit unit
+                                  (function
+                                   | MainArch -> I51 ()
+                                   | NewArch -> I52 ()
+                                   | ScratchArch -> I53 ()
+                                   | Lock -> I54 ()
+                                   | FPCache -> I55 ())
+                                  (function
+                                   | I51 () -> MainArch
+                                   | I52 () -> NewArch
+                                   | I53 () -> ScratchArch
+                                   | I54 () -> Lock
+                                   | I55 () -> FPCache))
+
 let showArchiveName =
   Prefs.createBool "showarchive" false
-    "!show 'true names' (for rootalias) of roots and archive"
+    ~category:(`Advanced `General)
+    "show 'true names' (for rootalias) of roots and archive"
     ("When this preference is set, Unison will print out the 'true names'"
-     ^ "of the roots, in the same form as is expected by the {\\tt rootalias}"
+     ^ "of the roots, in the same form as is expected by the {\\tt rootalias} "
      ^ "preference.")
 
 let _ = Prefs.alias showArchiveName "showArchiveName"
@@ -193,7 +240,7 @@ let archiveHash fspath =
   debugverbose (fun()-> Util.msg "Archive name is %s; hashcode is %s\n" n d);
   if Prefs.read showArchiveName then
     Util.msg "Archive name is %s; hashcode is %s\n" n d;
-  (String.sub d 0 significantDigits)
+  d
 
 (* We include the hash part of the archive name in the names of temp files
    created by this run of Unison.  The reason for this is that, during
@@ -213,6 +260,39 @@ let archiveName fspath (v: archiveVersion): string * string =
   (Printf.sprintf "%s%s" temp n,
    thisRootsGlobalName fspath)
 
+(* IMPORTANT!
+   This is the 2.51-compatible version of [archiveName]. It must produce
+   exactly the same result as [archiveName] would in version 2.51.5.
+   If code changes elsewhere make this function produce a different result then
+   it must be updated accordingly to again return the 2.51-compatible result.
+
+   This code is here only to support a smooth upgrade from versions <= 2.51.5
+   It is safe to delete it when that support is no longer required. *)
+let archiveName251 fspath (v: archiveVersion): string * string =
+  let archiveHash251 fspath =
+    (* How many characters of the filename should be used for the unique id of
+       the archive?  On Unix systems, we use the full fingerprint (32 bytes).
+       On windows systems, filenames longer than 8 bytes can cause problems, so
+       we chop off all but the first 6 from the fingerprint. *)
+    let significantDigits =
+      match Util.osType with
+        `Win32 -> 6
+      | `Unix -> 32
+    in
+    let thisRoot = thisRootsGlobalName fspath in
+    let r = Prefs.read rootsName in
+    let n = Printf.sprintf "%s;%s;22" thisRoot r in
+    let d = Fingerprint.toString (Fingerprint.string n) in
+    (String.sub d 0 significantDigits)
+  in
+  let n = archiveHash251 fspath in
+  let temp = match v with
+    MainArch -> "ar" | NewArch -> "tm" | ScratchArch -> "sc"
+  | Lock     -> "lk" | FPCache   -> "fp"
+  in
+  (Printf.sprintf "%s%s" temp n,
+   thisRootsGlobalName fspath)
+
 
 (*****************************************************************************)
 (*                             SANITY CHECKS                                 *)
@@ -262,6 +342,48 @@ let rec checkArchive
   | NoArchive ->
       135
 
+(* IMPORTANT!
+   This is the 2.51-compatible version of [checkArchive]. It must produce
+   exactly the same result as [checkArchive] in version 2.51.5.
+   If code changes elsewhere make this function produce a different result then
+   it must be updated accordingly to again return the 2.51-compatible result. *)
+let rec checkArchive251
+      (top: bool) (path: Name.t list) (arch: archive251) (h: int): int =
+  match arch with
+    ArchiveDir (desc, children) ->
+      begin match NameMap.validate children with
+        `Ok ->
+          ()
+      | `Duplicate nm ->
+          let path =
+            List.fold_right (fun n p -> Path.child p n) path Path.empty in
+          raise
+            (Util.Fatal (Printf.sprintf
+                           "Corrupted archive: \
+                            the file %s occurs twice in path %s"
+                           (Name.toString nm) (Path.toString path)));
+      | `Invalid (nm, nm') ->
+          let path =
+            List.fold_right (fun n p -> Path.child p n) path Path.empty in
+          raise
+            (Util.Fatal (Printf.sprintf
+                           "Corrupted archive: the files %s and %s are not \
+                            correctly ordered in directory %s"
+                           (Name.toString nm) (Name.toString nm')
+                           (Path.toString path)));
+      end;
+      NameMap.fold
+        (fun n a h ->
+           Uutil.hash2 (Name.hash n)
+                       (checkArchive251 false (n :: path) a h))
+        children (Props.hash251 desc h)
+  | ArchiveFile (desc, dig, _, ress) ->
+      Uutil.hash2 (Uutil.hash dig) (Props.hash251 desc h)
+  | ArchiveSymlink content ->
+      Uutil.hash2 (Uutil.hash content) h
+  | NoArchive ->
+      135
+
 (* [archivesIdentical l] returns true if all elements in [l] are the
    same and distinct from None *)
 let archivesIdentical l =
@@ -273,7 +395,7 @@ let (archiveNameOnRoot
        : Common.root ->  archiveVersion -> (string * string * bool) Lwt.t)
     =
   Remote.registerRootCmd
-    "archiveName"
+    "archiveName" marchiveVersion Umarshal.(prod3 string string bool id id)
       (fun (fspath, v) ->
        let (name,_) = archiveName fspath v in
        Lwt.return
@@ -290,11 +412,18 @@ let (archiveNameOnRoot
    archiveFormat and root names.  They appear in the header of the archive
    files *)
 let formatString = Printf.sprintf "Unison archive format %d" archiveFormat
+let compatFormatString = "Unison archive format 22"
+(* Every supported version released prior to the new archive encoding
+   uses this archive format string. *)
 
 let verboseArchiveName thisRoot =
   Printf.sprintf "Archive for root %s synchronizing roots %s"
     thisRoot (Prefs.read rootsName)
 
+let mpayload = Umarshal.prod4
+                 marchive Umarshal.int Umarshal.string Proplist.m
+                 Umarshal.id Umarshal.id
+
 (* Load in the archive in [fspath]; check that archiveFormat (first line)
    and roots (second line) match skip the third line (time stamp), and read
    in the archive *)
@@ -305,6 +434,10 @@ let loadArchiveLocal fspath (thisRoot: string) :
   Util.convertUnixErrorsToFatal "loading archive" (fun () ->
     if System.file_exists fspath then
       let c = System.open_in_bin fspath in
+      let close_on_error f =
+        try f () with e -> close_in_noerr c; raise e
+      in
+      close_on_error (fun () ->
       let header = input_line c in
       (* Sanity check on archive format *)
       if header<>formatString then begin
@@ -326,12 +459,88 @@ let loadArchiveLocal fspath (thisRoot: string) :
               I will delete the old archive and start from scratch.\n"
              roots (verboseArchiveName thisRoot));
         None
+      end else
+        let featrs =
+          match String.split_on_char '\030' (input_line c) with
+          | [] -> [] (* This is not possible, but compiler doesn't know it *)
+          | _ :: rest -> (* Ignore the first part of the timestamp line *)
+              Safelist.filter (fun x -> x <> "") rest
+        in
+        let commonFts = Features.inter featrs (Features.all ()) in
+        if Safelist.length featrs <> Safelist.length commonFts then
+          raise
+            (Util.Fatal ("Archive format mismatch: the archive was stored with \
+                         features that are currently not available.\n\
+                         Missing features: "
+                         ^ (String.concat ", " (Safelist.filter
+                             (fun x -> not (Safelist.mem x commonFts)) featrs))
+                         ^ "\nArchive file: "
+                         ^ fspath ^ "\n\
+                         You should either upgrade Unison or invoke Unison \
+                         once with -ignorearchives flag and then try again."));
+        try
+          (* Temporarily enable features that were used when storing the archive
+             to make sure the types are correct when loading the archive. *)
+          let negotiatedFts = Features.getEnabled () in
+          let () = Features.setEnabled commonFts in
+          (* Load the datastructure *)
+          let ((archive, hash, magic, properties) : archive * int * string * Proplist.t) =
+            Umarshal.from_channel mpayload c in
+          close_in c;
+          (* Restore to the negotiated features *)
+          let () = Features.setEnabled negotiatedFts in
+          Some (archive, hash, magic, properties)
+        with Failure s | Umarshal.Error s -> raise (Util.Fatal (Printf.sprintf
+           "Archive file seems damaged (%s): \
+            use the -ignorearchives option, or \
+            throw away archives on both machines and try again" s)))
+    else
+      (debug (fun() ->
+         Util.msg "Archive %s not found\n"
+           (System.fspathToDebugString fspath));
+      None))
+
+(* IMPORTANT!
+   This is the 2.51-compatible version of [loadArchiveLocal]. It must remain
+   capable of reading archives written by version 2.51.5. Be careful, as code
+   changes elsewhere may break this function unintentionally.
+
+   This code is here only to support a smooth upgrade from versions <= 2.51.5
+   It is safe to delete it when that support is no longer required. *)
+let loadArchiveLocal251 fspath (thisRoot: string) :
+    (archive * int * string * Proplist.t) option =
+  debug (fun() ->
+    Util.msg "Loading archive from %s\n" (System.fspathToDebugString fspath));
+  Util.convertUnixErrorsToFatal "loading archive" (fun () ->
+    if System.file_exists fspath then
+      let c = System.open_in_bin fspath in
+      let header = input_line c in
+      (* Sanity check on archive format *)
+      if header<>compatFormatString then begin
+        debug (fun () ->
+          Util.msg
+             "Archive format mismatch: found\n '%s'\n\
+              but expected\n '%s'.\n\
+              I will delete the old archive and start from scratch.\n"
+             header compatFormatString);
+        None
+      end else
+      let roots = input_line c in
+      (* Sanity check on roots. *)
+      if roots <> verboseArchiveName thisRoot then begin
+        debug (fun () ->
+          Util.msg
+             "Archive mismatch: found\n '%s'\n\
+              but expected\n '%s'.\n\
+              I will delete the old archive and start from scratch.\n"
+             roots (verboseArchiveName thisRoot));
+        None
       end else
         (* Throw away the timestamp line *)
         let _ = input_line c in
         (* Load the datastructure *)
         try
-          let ((archive, hash, magic) : archive * int * string) =
+          let ((archive, hash, magic) : archive251 * int * string) =
             Marshal.from_channel c in
           let properties =
             try
@@ -341,7 +550,7 @@ let loadArchiveLocal fspath (thisRoot: string) :
               Proplist.empty
           in
           close_in c;
-          Some (archive, hash, magic, properties)
+          Some (of_compat251 archive, hash, magic, properties)
         with Failure s -> raise (Util.Fatal (Printf.sprintf
            "Archive file seems damaged (%s): \
             use the -ignorearchives option, or throw away archives on both machines and try again" s))
@@ -360,35 +569,60 @@ let storeArchiveLocal fspath thisRoot archive hash magic properties =
      System.open_out_gen
        [Open_wronly; Open_creat; Open_trunc; Open_binary] 0o600 fspath
    in
+   let close_on_error f =
+     try f () with e -> close_out_noerr c; raise e
+   in
+   close_on_error (fun () ->
    output_string c formatString;
    output_string c "\n";
    output_string c (verboseArchiveName thisRoot);
    output_string c "\n";
-   (* This third line is purely informative *)
-   output_string c (Printf.sprintf "Written at %s - %s mode\n"
+   (* First part of third line is purely informative *)
+   output_string c (Printf.sprintf "Written at %s - %s mode"
                       (Util.time2string (Util.time()))
                       ((Case.ops())#modeDesc));
-   Marshal.to_channel c (archive, hash, magic) [Marshal.No_sharing];
-   output_char c '\000'; (* Marker that indicates that the archive
-                            is followed by a property list *)
-   Marshal.to_channel c properties [Marshal.No_sharing];
-   close_out c)
+   (* Second part of third line is not informative.
+      Record the features that change the archive format and must exist to
+      be able to load the archive later. *)
+   output_string c "\030";
+   output_string c (String.concat "\030" (Features.changingArchiveFormat ()));
+   output_string c "\n";
+   Umarshal.to_channel mpayload c (archive, hash, magic, properties);
+   close_out c))
 
-(* Remove the archieve under the root path [fspath] with archiveVersion [v] *)
+(* IMPORTANT! This val is here for smoother upgrades from versions <= 2.51.5
+   It can be removed when this compatibility is no longer required. *)
+let loadedCompatArchive = ref []
+
+(* Remove the archive under the root path [fspath] with archiveVersion [v] *)
 let removeArchiveLocal ((fspath: Fspath.t), (v: archiveVersion)): unit Lwt.t =
-  Lwt.return
-    (let (name,_) = archiveName fspath v in
+  let f' name = Lwt.return (
      let fspath = Util.fileInUnisonDir name in
      debug (fun() ->
        Util.msg "Removing archive %s\n" (System.fspathToDebugString fspath));
      Util.convertUnixErrorsToFatal "removing archive" (fun () ->
        try System.unlink fspath
        with Unix.Unix_error (Unix.ENOENT, _, _) -> ()))
+  in
+  let ret = f' (fst (archiveName fspath v)) in
+  (* IMPORTANT! This code is for smoother upgrades from versions <= 2.51.5
+     It can be removed when this compatibility is no longer required. *)
+  if Safelist.exists (fun x -> x = fspath) !loadedCompatArchive then begin
+    loadedCompatArchive := Safelist.filter (fun x -> x <> fspath)
+      !loadedCompatArchive;
+    (try
+      ignore (f' (fst (archiveName251 fspath MainArch)))
+    with Util.Fatal _ -> ());
+    try
+      ignore (f' (fst (archiveName251 fspath FPCache)))
+    with Util.Fatal _ -> ()
+  end;
+  ret
 
 (* [removeArchiveOnRoot root v] invokes [removeArchive fspath v] on the
    server, where [fspath] is the path to root on the server *)
 let removeArchiveOnRoot: Common.root -> archiveVersion -> unit Lwt.t =
-  Remote.registerRootCmd "removeArchive" removeArchiveLocal
+  Remote.registerRootCmd "removeArchive" marchiveVersion Umarshal.unit removeArchiveLocal
 
 (* [commitArchive (fspath, ())] commits the archive for [fspath] by changing
    the filenames from ScratchArch-ones to a NewArch-ones *)
@@ -406,7 +640,15 @@ let commitArchiveLocal ((fspath: Fspath.t), ())
 (* [commitArchiveOnRoot root v] invokes [commitArchive fspath v] on the
    server, where [fspath] is the path to root on the server *)
 let commitArchiveOnRoot: Common.root -> unit -> unit Lwt.t =
-  Remote.registerRootCmd "commitArchive" commitArchiveLocal
+  Remote.registerRootCmd "commitArchive" Umarshal.unit Umarshal.unit commitArchiveLocal
+
+let getArchiveInfo f =
+  Util.convertUnixErrorsToTransient "querying file information"
+    (fun () ->
+       try
+         Some (System.stat f)
+       with Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
+         None)
 
 let archiveInfoCache = Hashtbl.create 7
 (* [postCommitArchive (fspath, v)] finishes the committing protocol by
@@ -432,20 +674,27 @@ let postCommitArchiveLocal (fspath,())
          let outFd =
            System.open_out_gen
              [Open_wronly; Open_creat; Open_trunc; Open_binary] 0o600 fto in
+         let close_on_error f =
+           try f () with e -> close_out_noerr outFd; raise e
+         in
+         close_on_error (fun () ->
          System.chmod fto 0o600; (* In case the file already existed *)
          let inFd = System.open_in_bin ffrom in
+         let close_on_error f =
+           try f () with e -> close_in_noerr inFd; raise e
+         in
+         close_on_error (fun () ->
          Uutil.readWrite inFd outFd (fun _ -> ());
          close_in inFd;
-         close_out outFd
+         close_out outFd))
        end;
        let arcFspath = Util.fileInUnisonDir toname in
-       let info = Fileinfo.get' arcFspath in
-       Hashtbl.replace archiveInfoCache thisRoot info))
+       Hashtbl.replace archiveInfoCache thisRoot (getArchiveInfo arcFspath)))
 
 (* [postCommitArchiveOnRoot root v] invokes [postCommitArchive fspath v] on
    the server, where [fspath] is the path to root on the server *)
 let postCommitArchiveOnRoot: Common.root -> unit -> unit Lwt.t =
-  Remote.registerRootCmd "postCommitArchive" postCommitArchiveLocal
+  Remote.registerRootCmd "postCommitArchive" Umarshal.unit Umarshal.unit postCommitArchiveLocal
 
 
 (*************************************************************************)
@@ -477,24 +726,74 @@ let setArchivePropsLocal (thisRoot: string) (props: Proplist.t) =
   Hashtbl.replace archivePropCache thisRoot props
 
 let fileUnchanged oldInfo newInfo =
-  oldInfo.Fileinfo.typ = `FILE && newInfo.Fileinfo.typ = `FILE
-    &&
-  Props.same_time oldInfo.Fileinfo.desc newInfo.Fileinfo.desc
-    &&
-  Props.length oldInfo.Fileinfo.desc = Props.length newInfo.Fileinfo.desc
-    &&
-  match Fileinfo.stamp oldInfo, Fileinfo.stamp newInfo with
-    Fileinfo.InodeStamp in1, Fileinfo.InodeStamp in2 -> in1 = in2
-  | Fileinfo.CtimeStamp _,   Fileinfo.CtimeStamp _   -> true
-  | _                                                -> false
-
-let archiveUnchanged fspath newInfo =
-  let (arcName, thisRoot) = archiveName fspath MainArch in
+  match oldInfo, newInfo with
+  | None, _ | _, None -> false
+  | Some o, Some n ->
+      o.Unix.LargeFile.st_kind = S_REG && n.Unix.LargeFile.st_kind = S_REG
+        &&
+      o.Unix.LargeFile.st_mtime = n.Unix.LargeFile.st_mtime
+        &&
+      o.Unix.LargeFile.st_size = n.Unix.LargeFile.st_size
+        &&
+      (o.Unix.LargeFile.st_ino = n.Unix.LargeFile.st_ino
+          ||
+        Prefs.read Fileinfo.ignoreInodeNumbers
+          ||
+        not (System.hasInodeNumbers ()))
+
+let archiveUnchanged thisRoot newInfo =
   try
     fileUnchanged (Hashtbl.find archiveInfoCache thisRoot) newInfo
   with Not_found ->
     false
 
+
+(*************************************************************************)
+(*                     Shared props data in archive                      *)
+(*************************************************************************)
+
+let debugpd = Util.debug "propsdata+"
+
+let propsDataKey = Proplist.register "props data" Props.Data.m
+
+let prunePropsdata archive =
+  (* Do propsdata-GC by keeping live props *)
+  let rec prunePropsdata = function
+    | ArchiveDir (props, children) ->
+        Props.Data.gcKeep props;
+        NameMap.iter (fun _ c -> prunePropsdata c) children
+    | ArchiveFile (props, _, _, _) ->
+        Props.Data.gcKeep props
+    | ArchiveSymlink _ -> ()
+    | NoArchive -> ()
+  in
+  let t0 = Unix.gettimeofday () in
+  debugpd (fun () -> Util.msg "Pruning shared props data...\n");
+  Props.Data.gcInit ();
+  prunePropsdata archive;
+  let pd = Props.Data.gcDone () in
+  debugpd (fun () ->
+    let t1 = Unix.gettimeofday () in
+    Util.msg "Shared props data pruning took %.3f milliseconds\n"
+      ((t1 -. t0) *. 1000.));
+  pd
+
+let externArchivePropsdata archive props =
+  match prunePropsdata archive with
+  | [] -> props
+  | pd -> Proplist.add propsDataKey pd props
+
+let internArchivePropsdata props =
+  let t0 = Unix.gettimeofday () in
+  debugpd (fun () -> Util.msg "Restoring shared props data...\n");
+  let data = try Proplist.find propsDataKey props with Not_found -> [] in
+  Props.Data.intern data;
+  debugpd (fun () ->
+    let t1 = Unix.gettimeofday () in
+    Util.msg "Shared props data restoring took %.3f milliseconds\n"
+      ((t1 -. t0) *. 1000.))
+
+
 (*************************************************************************
                            DUMPING ARCHIVES
  *************************************************************************)
@@ -523,6 +822,10 @@ let dumpArchiveLocal (fspath,()) =
   debug (fun () -> Printf.eprintf "Dumping archive into `%s'\n"
                      (System.fspathToDebugString f));
   let ch = System.open_out_gen [Open_wronly; Open_creat; Open_trunc] 0o600 f in
+  let close_on_error f =
+    try f () with e -> close_out_noerr ch; raise e
+  in
+  close_on_error (fun () ->
   let (outfn,flushfn) = Format.get_formatter_output_functions () in
   Format.set_formatter_out_channel ch;
   Format.printf "Contents of archive for %s\n" root;
@@ -531,11 +834,11 @@ let dumpArchiveLocal (fspath,()) =
   Format.print_flush();
   Format.set_formatter_output_functions outfn flushfn;
   flush ch;
-  close_out ch;
+  close_out ch);
   Lwt.return ()
 
 let dumpArchiveOnRoot : Common.root -> unit -> unit Lwt.t =
-  Remote.registerRootCmd "dumpArchive" dumpArchiveLocal
+  Remote.registerRootCmd "dumpArchive" Umarshal.unit Umarshal.unit dumpArchiveLocal
 
 (*****************************************************************************)
 (*                          ARCHIVE CASE CONVERSION                          *)
@@ -543,10 +846,10 @@ let dumpArchiveOnRoot : Common.root -> unit -> unit Lwt.t =
 
 (* Stamp for marking unchange directories *)
 let dirStampKey : Props.dirChangedStamp Proplist.key =
-  Proplist.register "unchanged directory stamp"
+  Proplist.register "unchanged directory stamp" Props.mdirChangedStamp
 
 (* Property containing a description of the archive case sensitivity mode *)
-let caseKey : string Proplist.key = Proplist.register "case mode"
+let caseKey : string Proplist.key = Proplist.register "case mode" Umarshal.string
 
 (* Turn a case sensitive archive into a case insensitive archive.
    Directory children are resorted and duplicates are removed.
@@ -579,7 +882,7 @@ let makeCaseSensitive thisRoot =
           (getArchiveProps thisRoot)))
 
 let makeCaseSensitiveOnRoot =
-  Remote.registerRootCmd "makeCaseSensitive"
+  Remote.registerRootCmd "makeCaseSensitive" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) ->
        makeCaseSensitive (thisRootsGlobalName fspath);
        Lwt.return ())
@@ -664,6 +967,7 @@ let setArchiveData thisRoot fspath (arch, hash, magic, properties) info =
   let properties = Proplist.add caseKey archMode properties in
   setArchiveLocal thisRoot arch;
   setArchivePropsLocal thisRoot properties;
+  internArchivePropsdata properties;
   Hashtbl.replace archiveInfoCache thisRoot info;
   if archMode <> curMode then populateCacheFromArchive fspath arch;
   Lwt.return (Some (hash, magic))
@@ -672,13 +976,14 @@ let clearArchiveData thisRoot =
   setArchiveLocal thisRoot NoArchive;
   setArchivePropsLocal thisRoot
     (Proplist.add caseKey (Case.ops ())#modeDesc Proplist.empty);
+  internArchivePropsdata Proplist.empty;
   Hashtbl.remove archiveInfoCache thisRoot;
   Lwt.return (Some (0, ""))
 
 (* Load (main) root archive and cache it on the given server *)
 let loadArchiveOnRoot: Common.root -> bool -> (int * string) option Lwt.t =
   Remote.registerRootCmd
-    "loadArchive"
+    "loadArchive" Umarshal.bool Umarshal.(option (prod2 int string id id))
     (fun (fspath, optimistic) ->
        let (arcName,thisRoot) = archiveName fspath MainArch in
        let arcFspath = Util.fileInUnisonDir arcName in
@@ -700,38 +1005,59 @@ let loadArchiveOnRoot: Common.root -> bool -> (int * string) option Lwt.t =
          then
            Lwt.return None
          else
-           let (arcName,thisRoot) = archiveName fspath MainArch in
-           let arcFspath = Util.fileInUnisonDir arcName in
-           let info = Fileinfo.get' arcFspath in
-           if archiveUnchanged fspath info then
+           let info = getArchiveInfo arcFspath in
+           if archiveUnchanged thisRoot info then
              (* The archive is unchanged.  So, we don't need to do
                 anything. *)
              Lwt.return (Some (0, ""))
            else begin
              match loadArchiveLocal arcFspath thisRoot with
                Some archData ->
-                 let info' = Fileinfo.get' arcFspath in
+                 let info' = getArchiveInfo arcFspath in
                  if fileUnchanged info info' then
                    setArchiveData thisRoot fspath archData info
                  else
                    (* The archive was modified during loading.  We fail. *)
                    Lwt.return None
              | None ->
-                   (* No archive found *)
-                   Lwt.return None
+                   (* No archive found, try 2.51 upgrade mode *)
+                   (* IMPORTANT! This code is for smoother upgrades from
+                      versions <= 2.51.5
+                      It can be removed when this compatibility is no longer
+                      required. *)
+                   let (arcName, thisRoot) = archiveName251 fspath MainArch in
+                   let arcFspath = Util.fileInUnisonDir arcName in
+                   match loadArchiveLocal251 arcFspath thisRoot with
+                   | Some archData ->
+                       loadedCompatArchive := fspath :: !loadedCompatArchive;
+                       setArchiveData thisRoot fspath archData
+                         (getArchiveInfo arcFspath)
+                   | None -> Lwt.return None
            end
        end else begin
          match loadArchiveLocal arcFspath thisRoot with
            Some archData ->
-             setArchiveData thisRoot fspath archData (Fileinfo.get' arcFspath)
+             setArchiveData thisRoot fspath archData (getArchiveInfo arcFspath)
          | None ->
-             (* No archive found *)
-             clearArchiveData thisRoot
+             (* No archive found, try 2.51 upgrade mode *)
+             (* IMPORTANT! This code is for smoother upgrades from
+                versions <= 2.51.5
+                It can be removed when this compatibility is no longer
+                required. *)
+             let (arcName, thisRoot) = archiveName251 fspath MainArch in
+             let arcFspath = Util.fileInUnisonDir arcName in
+             match loadArchiveLocal251 arcFspath thisRoot with
+             | Some archData ->
+                 loadedCompatArchive := fspath :: !loadedCompatArchive;
+                 setArchiveData thisRoot fspath archData (getArchiveInfo arcFspath)
+             | None -> clearArchiveData thisRoot
        end)
 
 let dumpArchives =
   Prefs.createBool "dumparchives" false
-    "*dump contents of archives just after loading"
+    ~category:`Expert
+    ~cli_only:true
+    "dump contents of archives just after loading"
     ("When this preference is set, Unison will create a file unison.dump "
      ^ "on each host, containing a text summary of the archive, immediately "
      ^ "after loading it.")
@@ -750,13 +1076,14 @@ let loadArchives (optimistic: bool) =
       ^ "OCaml version 3 and one with OCaml version 4.\n"
       ^ "\n"
       ^ "If this is not the case and you get this message repeatedly, please:\n"
-      ^ "  a) Send a bug report to unison-users@yahoogroups.com (you may need\n"
+      ^ "  a) Send a bug report to unison-users@seas.upenn.edu (you may need\n"
       ^ "     to join the group before you will be allowed to post).\n"
+      ^ "     For information, see https://github.com/bcpierce00/unison/wiki\n"
       ^ "  b) Move the archive files on each machine to some other directory\n"
       ^ "     (in case they may be useful for debugging).\n"
       ^ "     The archive files on this machine are in the directory\n"
       ^ (Printf.sprintf "       %s\n"
-           (System.fspathToPrintString Util.unisonDir))
+           Util.unisonDir)
       ^ "     and have names of the form\n"
       ^ "       arXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n"
       ^ "     where the X's are hexadecimal numbers.\n"
@@ -775,11 +1102,11 @@ let lockArchiveLocal fspath =
     None
   else
     Some (Printf.sprintf "The file %s on host %s should be deleted"
-            (System.fspathToPrintString lockFile) (Os.myCanonicalHostName ()))
+            lockFile (Os.myCanonicalHostName ()))
 
 let lockArchiveOnRoot: Common.root -> unit -> string option Lwt.t =
   Remote.registerRootCmd
-    "lockArchive" (fun (fspath, ()) -> Lwt.return (lockArchiveLocal fspath))
+    "lockArchive" Umarshal.unit Umarshal.(option string) (fun (fspath, ()) -> Lwt.return (lockArchiveLocal fspath))
 
 let unlockArchiveLocal fspath =
   Lock.release
@@ -787,12 +1114,13 @@ let unlockArchiveLocal fspath =
 
 let unlockArchiveOnRoot: Common.root -> unit -> unit Lwt.t =
   Remote.registerRootCmd
-    "unlockArchive"
+    "unlockArchive" Umarshal.unit Umarshal.unit
     (fun (fspath, ()) -> Lwt.return (unlockArchiveLocal fspath))
 
 let ignorelocks =
   Prefs.createBool "ignorelocks" false
-    "!ignore locks left over from previous run (dangerous!)"
+    ~category:(`Advanced `General)
+    "ignore locks left over from previous run (dangerous!)"
     ("When this preference is set, Unison will ignore any lock files "
      ^ "that may have been left over from a previous run of Unison that "
      ^ "was interrupted while reading or writing archive files; by default, "
@@ -871,7 +1199,7 @@ let unlockArchives () =
 
 let archivesExistOnRoot: Common.root -> unit -> (bool * bool) Lwt.t =
   Remote.registerRootCmd
-    "archivesExist"
+    "archivesExist" Umarshal.unit Umarshal.(prod2 bool bool id id)
     (fun (fspath,rootsName) ->
        let (oldname,_) = archiveName fspath MainArch in
        let oldexists =
@@ -879,6 +1207,16 @@ let archivesExistOnRoot: Common.root -> unit -> (bool * bool) Lwt.t =
        let (newname,_) = archiveName fspath NewArch in
        let newexists =
          System.file_exists (Util.fileInUnisonDir newname) in
+       let oldexists =
+         if oldexists || newexists then oldexists else
+           (* No archive found, try 2.51 upgrade mode *)
+           (* IMPORTANT! This code is for smoother upgrades from
+              versions <= 2.51.5
+              It can be removed when this compatibility is no longer
+              required. *)
+           let (oldname, _) = archiveName251 fspath MainArch in
+           System.file_exists (Util.fileInUnisonDir oldname)
+       in
        Lwt.return (oldexists, newexists))
 
 let forall = Safelist.for_all (fun x -> x)
@@ -959,9 +1297,6 @@ let doArchiveCrashRecovery () =
      ^ "between synchronizations.  See the documentation for the UNISONLOCALHOSTNAME\n"
      ^ "environment variable for advice on how to correct this.\n"
      ^ "\n"
-     ^ "Donations to the Unison project are gratefully accepted: \n"
-     ^ "http://www.cis.upenn.edu/~bcpierce/unison\n"
-     ^ "\n"
      (* ^ "\nThe expected archive names were:\n" ^ expectedNames *) );
     Lwt.return ()
   end))
@@ -1038,7 +1373,7 @@ let translatePathLocal fspath path =
   localPath
 
 let translatePath =
-  Remote.registerRootCmd "translatePath"
+  Remote.registerRootCmd "translatePath" Path.m Path.mlocal
     (fun (fspath, path) -> Lwt.return (translatePathLocal fspath path))
 
 (***********************************************************************
@@ -1047,7 +1382,8 @@ let translatePath =
 
 let mountpoints =
   Prefs.createStringList "mountpoint"
-    "!abort if this path does not exist"
+    ~category:(`Advanced `General)
+    "abort if this path does not exist"
     ("Including the preference \\texttt{-mountpoint PATH} causes Unison to "
      ^ "double-check, at the end of update detection, that \\texttt{PATH} exists "
      ^ "and abort if it does not.  This is useful when Unison is used to synchronize "
@@ -1134,7 +1470,8 @@ let rec getSubTree path tree =
 
 let fastcheck =
   Prefs.createBoolWithDefault "fastcheck"
-    "!do fast update detection (true/false/default)"
+    ~category:(`Advanced `Syncprocess)
+    "do fast update detection (true/false/default)"
     ( "When this preference is set to \\verb|true|, \
        Unison will use the modification time and length of a file as a
        `pseudo inode number' \
@@ -1161,7 +1498,8 @@ let useFastChecking () =
       Prefs.read fastcheck = `True
    || (Prefs.read fastcheck = `Default (*&& Util.osType = `Unix*))
 
-let immutable = Pred.create "immutable" ~advanced:true
+let immutable = Pred.create "immutable"
+  ~category:(`Advanced `Sync)
   ("This preference specifies paths for directories whose \
      immediate children are all immutable files --- i.e., once a file has been \
      created, its contents never changes.  When scanning for updates, \
@@ -1169,13 +1507,15 @@ let immutable = Pred.create "immutable" ~advanced:true
      this can speed update detection significantly (in particular, for mail \
      directories).")
 
-let immutablenot = Pred.create "immutablenot" ~advanced:true
+let immutablenot = Pred.create "immutablenot"
+  ~category:(`Advanced `Sync)
   ("This preference overrides {\\tt immutable}.")
 
 type scanInfo =
     { fastCheck : bool;
       dirFastCheck : bool;
       dirStamp : Props.dirChangedStamp;
+      rescanProps : bool;
       archHash : string;
       showStatus : bool }
 
@@ -1263,7 +1603,7 @@ let rec noChildChange childUpdates =
 (* Check whether the directory contents is different from what is in
    the archive *)
 let directoryCheckContentUnchanged
-      currfspath path info archDesc childUpdates scanInfo =
+      currfspath path info propsChanged archDesc childUpdates scanInfo =
   if
     noChildChange childUpdates
       &&
@@ -1275,13 +1615,23 @@ let directoryCheckContentUnchanged
       let inode =
         match Fileinfo.stamp info with Fileinfo.InodeStamp i -> i | _ -> 0 in
       Props.setDirChangeFlag archDesc scanInfo.dirStamp inode in
-    let updated =
-      updated || not (Props.same_time info.Fileinfo.desc archDesc) in
     if updated then
       debugverbose (fun()->
         Util.msg "Contents of directory %s marked unchanged\n"
           (Fspath.toDebugString (Fspath.concat currfspath path)));
-    (Props.setTime archDesc (Props.time info.Fileinfo.desc), updated)
+    (* Only update the times in archive if there is nothing to propagate for
+       the dir itself. Otherwise, if propagation fails and times in archive
+       are updated anyway then the changes that failed to propagate may be
+       missed at the next scan. If there is something to propagate then all
+       archive changes must go through propagation. With the exception of
+       dirChangeFlag, which is safe to update without updating mtime. *)
+    if propsChanged then
+      (archDesc, updated)
+    else
+      let updated =
+        updated || not (Props.same_time info.Fileinfo.desc archDesc)
+          || not (Props.same_ctime info.desc archDesc) in
+      (Props.setTime archDesc info.Fileinfo.desc, updated)
   end else begin
     let (archDesc, updated) =
       Props.setDirChangeFlag archDesc Props.changedDirStamp 0 in
@@ -1337,8 +1687,10 @@ let checkContentsChange
                Fileinfo.InodeStamp inode ->
                  (Util.msg "archStamp is inode (%d)" inode;
                   Util.msg " / info.inode (%d)" info.Fileinfo.inode)
-             | Fileinfo.CtimeStamp stamp ->
-                 (Util.msg "archStamp is ctime (%f)" stamp)
+             | Fileinfo.NoStamp ->
+                 (Util.msg "archStamp is no-stamp")
+             | Fileinfo.RescanStamp ->
+                 (Util.msg "archStamp is rescan-possibly-updated")
            end;
            Util.msg " / times: %f = %f... %b"
              (Props.time archDesc) (Props.time info.Fileinfo.desc)
@@ -1355,7 +1707,19 @@ let checkContentsChange
   in
   if dataClearlyUnchanged && ressClearlyUnchanged then begin
     Xferhint.insertEntry currfspath path archFp;
-    None, checkPropChange info.Fileinfo.desc archive archDesc
+    let propsUpdates = checkPropChange info.Fileinfo.desc archive archDesc in
+    let propsChanged = propsUpdates <> NoUpdates in
+    (* Only update the archive if there is nothing to propagate. Otherwise,
+       if propagation fails and times in archive are updated anyway then the
+       changes that failed to propagate may be missed at the next scan. *)
+    let optArch =
+      if propsChanged || Props.same_ctime info.Fileinfo.desc archDesc then
+        None
+      else
+        let newprops = Props.setTime archDesc info.Fileinfo.desc in
+        Some (ArchiveFile (newprops, archFp, archStamp, archRess))
+    in
+    optArch, propsUpdates
   end else begin
     debugverbose (fun() -> Util.msg "  Double-check possibly updated file\n");
     showStatusAddLength scanInfo info;
@@ -1367,12 +1731,20 @@ let checkContentsChange
              (Os.fullfingerprint_to_string archFp)
              (Os.fullfingerprint_to_string newFp));
     if archFp = newFp then begin
-      let newprops = Props.setTime archDesc (Props.time newDesc) in
+      let propsUpdates = checkPropChange newDesc archive archDesc in
+      let propsChanged = propsUpdates <> NoUpdates in
+      (* Only update the archive if there is nothing to propagate. Otherwise,
+         if propagation fails and times in archive are updated anyway then the
+         changes that failed to propagate may be missed at the next scan. *)
+      begin if propsChanged then
+        None
+      else
+      let newprops = Props.setTime archDesc newDesc in
       let newarch = ArchiveFile (newprops, archFp, newStamp, newRess) in
       debugverbose (fun() ->
         Util.msg "  Contents match: update archive with new time...%f\n"
                    (Props.time newprops));
-      Some newarch, checkPropChange newDesc archive archDesc
+      Some newarch end, propsUpdates
     end else begin
       debug (fun() -> Util.msg "  Updated file\n");
       (* [BCP 5/2011] We might add a sanity check here: if the file contents
@@ -1593,7 +1965,13 @@ and buildUpdateRec archive currfspath path scanInfo =
     debug (fun() ->
       Util.msg "buildUpdateRec: %s\n"
         (Fspath.toDebugString (Fspath.concat currfspath path)));
-    let info = Fileinfo.get true currfspath path in
+    let archProps =
+      match scanInfo.fastCheck, scanInfo.rescanProps, archive with
+      | true, false, ArchiveFile (archDesc, _, _, _) -> Some archDesc
+      | true, false, ArchiveDir (archDesc, _) -> Some archDesc
+      | _ -> None
+    in
+    let info = Fileinfo.get ?archProps true currfspath path in
     match (info.Fileinfo.typ, archive) with
       (`ABSENT, NoArchive) ->
         debug (fun() -> Util.msg "  buildUpdate -> Absent and no archive\n");
@@ -1666,8 +2044,9 @@ and buildUpdateRec archive currfspath path scanInfo =
              (These are files or directories which used not to be
              ignored and are now ignored.) *)
           if hasIgnoredChildren then (archDesc, true) else
+          let propsChanged = permchange <> PropsSame in
           directoryCheckContentUnchanged
-            currfspath path info archDesc childUpdates scanInfo in
+            currfspath path info propsChanged archDesc childUpdates scanInfo in
         (begin match newChildren with
            Some ch ->
              Some (ArchiveDir (archDesc, ch))
@@ -1804,7 +2183,13 @@ let rec buildUpdate archive fspath fullpath here path pathTree scanInfo =
        end,
        ui, here, [])
   | Some(name, path') ->
-      let info = Fileinfo.get true fspath here in
+      let archProps =
+        match scanInfo.fastCheck, scanInfo.rescanProps, archive with
+        | true, false, ArchiveFile (archDesc, _, _, _) -> Some archDesc
+        | true, false, ArchiveDir (archDesc, _) -> Some archDesc
+        | _ -> None
+      in
+      let info = Fileinfo.get ?archProps true fspath here in
       if info.Fileinfo.typ <> `DIRECTORY && info.Fileinfo.typ <> `ABSENT then
         let error =
           if Path.isEmpty here then
@@ -1893,14 +2278,14 @@ let updatePredicates =
    ("follow", Path.followPred)]
 
 let predKey : (string * string list) list Proplist.key =
-  Proplist.register "update predicates"
-let rsrcKey : bool Proplist.key = Proplist.register "rsrc pref"
+  Proplist.register "update predicates" Umarshal.(list (prod2 string (list string) id id))
+let rsrcKey : bool Proplist.key = Proplist.register "rsrc pref" Umarshal.bool
 
-let checkNoUpdatePredicateChange thisRoot =
+let checkNoUpdatePredicateChange thisRoot rescanProps =
   let props = getArchiveProps thisRoot in
   let oldPreds = try Proplist.find predKey props with Not_found -> [] in
   let newPreds =
-    List.map (fun (nm, p) -> (nm, Pred.extern p)) updatePredicates in
+    Safelist.map (fun (nm, p) -> (nm, Pred.extern p)) updatePredicates in
 (*
 List.iter
   (fun (nm, l) ->
@@ -1913,7 +2298,8 @@ Format.eprintf "==> %b@." (oldPreds = newPreds);
     try Some (Proplist.find rsrcKey props) with Not_found -> None in
   let newRsrc = Prefs.read Osx.rsrc in
   try
-    if oldPreds <> newPreds || oldRsrc <> Some newRsrc then raise Not_found;
+    if oldPreds <> newPreds || oldRsrc <> Some newRsrc || rescanProps then
+      raise Not_found;
     Proplist.find dirStampKey props
   with Not_found ->
     let stamp = Props.freshDirStamp () in
@@ -1923,6 +2309,50 @@ Format.eprintf "==> %b@." (oldPreds = newPreds);
             (Proplist.add rsrcKey newRsrc props)));
     stamp
 
+(* All the predicates that may change the set of props scanned during
+   update detection *)
+let propsPredicates =
+  [ ("xattrignore", Props.xattrIgnorePred, Props.xattrEnabled);
+    ("xattrignorenot", Props.xattrIgnorenotPred, Props.xattrEnabled);
+  ]
+
+let pred2Key : (string * string list) list Proplist.key =
+  Proplist.register "props predicates" Umarshal.(list (prod2 string (list string) id id))
+let xattrsKey : bool Proplist.key = Proplist.register "xattrs pref" Umarshal.bool
+let aclKey : bool Proplist.key = Proplist.register "acl pref" Umarshal.bool
+
+let mustRescanProps thisRoot =
+  let props = getArchiveProps thisRoot in
+  let oldPreds = try Proplist.find pred2Key props with Not_found -> [] in
+  let newPreds =
+    Safelist.filterMap (fun (nm, p, c) ->
+      if c () then Some (nm, Pred.extern p) else None) propsPredicates in
+  let oldXattrs =
+    try Some (Proplist.find xattrsKey props) with Not_found -> None in
+  let newXattrs =
+    if Props.xattrEnabled () then Some (Prefs.read Props.syncXattrs) else None in
+  let oldACL =
+    try Some (Proplist.find aclKey props) with Not_found -> None in
+  let newACL =
+    if Props.aclEnabled () then Some (Prefs.read Props.syncACL) else None in
+  if oldPreds = newPreds && oldXattrs = newXattrs && oldACL = newACL then
+    false
+  else begin
+    let props =
+      match newACL with
+      | Some x -> Proplist.add aclKey x props
+      | None -> props in
+    let props =
+      match newXattrs with
+      | Some x -> Proplist.add xattrsKey x props
+      | None -> props in
+    let props =
+      if newPreds <> [] then Proplist.add pred2Key newPreds props
+      else props in
+    let () = setArchivePropsLocal thisRoot props in
+    newXattrs = Some true || newACL = Some true
+  end
+
 (* This contains the list of synchronized paths and the directory stamps
    used by the previous update detection, when a watcher process is used.
    This make it possible to know when the state of the watcher process
@@ -1946,7 +2376,8 @@ let findLocal wantWatcher fspath pathList subpaths :
      deleted.  --BCP 2006 *)
   let (arcName,thisRoot) = archiveName fspath MainArch in
   let archive = getArchive thisRoot in
-  let dirStamp = checkNoUpdatePredicateChange thisRoot in
+  let rescanProps = mustRescanProps thisRoot in
+  let dirStamp = checkNoUpdatePredicateChange thisRoot rescanProps in
 (*
 let t1 = Unix.gettimeofday () in
 *)
@@ -1956,7 +2387,7 @@ let t1 = Unix.gettimeofday () in
          as Windows does not update directory modification times
          on FAT filesystems. *)
       dirFastCheck = useFastChecking () && Util.osType = `Unix;
-      dirStamp = dirStamp; archHash = archiveHash fspath;
+      dirStamp; rescanProps; archHash = archiveHash fspath;
       showStatus = not !Trace.runningasserver }
   in
   let (cacheFilename, _) = archiveName fspath FPCache in
@@ -1971,7 +2402,8 @@ let t1 = Unix.gettimeofday () in
   in
   let paths =
     match subpaths with
-      Some (unsynchronizedPaths, blacklistedPaths) when unchangedOptions ->
+      Some (unsynchronizedPaths, blacklistedPaths) when unchangedOptions
+          && Fswatchold.running scanInfo.archHash ->
         let (>>) x f = f x in
         let paths =
           Fswatchold.getChanges scanInfo.archHash
@@ -2039,13 +2471,29 @@ Format.eprintf "Update detection: %f@." (t2 -. t1);
   abortIfAnyMountpointsAreMissing fspath;
   updates
 
+(* Conversion functions for 2.51-compatible return type:
+     (Path.local * Common.updateItem * Props.t list) list *)
+let convV0 = Remote.makeConvV0FunRet
+  (fun r -> Safelist.map
+    (fun (a, b, c) -> a, Common.ui_to_compat251 b, Safelist.map Props.to_compat251 c) r)
+  (fun r -> Safelist.map
+    (fun (a, b, c) -> a, Common.ui_of_compat251 b, Safelist.map Props.of_compat251 c) r)
+
 let findOnRoot =
   Remote.registerRootCmd
-    "find"
+    "find" ~convV0
+    Umarshal.(prod3 bool (list Path.m) (option (prod2 (list Path.m) (list Path.m) id id)) id id)
+    Umarshal.(list (prod3 Path.mlocal Common.mupdateItem (list Props.m) id id))
     (fun (fspath, (wantWatcher, pathList, subpaths)) ->
        Lwt.return (findLocal wantWatcher fspath pathList subpaths))
 
-let findUpdatesOnPaths ?wantWatcher pathList subpaths =
+let mergePropsdataOnRoot =
+  Remote.registerRootCmd "propsdata" Props.Data.m Props.Data.m
+  (fun (fspath, propsdata) ->
+     Props.Data.merge propsdata;
+     Lwt.return (Props.Data.extern `New))
+
+let findUpdatesOnPaths ?(wantWatcher=false) pathList subpaths =
   Lwt_unix.run
     (loadArchives true >>= (fun (ok, checksums) ->
      begin if ok then Lwt.return checksums else begin
@@ -2068,7 +2516,7 @@ let findUpdatesOnPaths ?wantWatcher pathList subpaths =
      let t = Trace.startTimer "Collecting changes" in
      Globals.allRootsMapWithWaitingAction (fun r ->
        debug (fun() -> Util.msg "findOnRoot %s\n" (root2string r));
-       findOnRoot r (wantWatcher <> None, pathList, subpaths))
+       findOnRoot r (wantWatcher, pathList, subpaths))
        (fun (host, _) ->
          begin match host with
            Remote _ -> Uutil.showUpdateStatus "";
@@ -2076,6 +2524,16 @@ let findUpdatesOnPaths ?wantWatcher pathList subpaths =
          | _        -> ()
          end)
        >>= (fun updates ->
+     begin Globals.allRootsIter (fun r ->
+       match r with
+       | (Local, _) -> Lwt.return ()
+       | (Remote _, _) when not (Props.Data.enabled ()) -> Lwt.return ()
+       | (Remote _, _) -> begin
+           mergePropsdataOnRoot r (Props.Data.extern `New) >>= fun propsdata ->
+           Props.Data.merge propsdata;
+           Lwt.return ()
+         end)
+     end >>= fun () ->
      Trace.showTimer t;
      let result =
        Safelist.map
@@ -2121,7 +2579,7 @@ let findUpdates ?wantWatcher subpaths =
 (*****************************************************************************)
 
 (* To prepare for committing, write to Scratch Archive *)
-let prepareCommitLocal (fspath, magic) =
+let prepareCommitLocal compatMode (fspath, magic) =
   let (newName, root) = archiveName fspath ScratchArch in
   let archive = getArchive root in
   (**
@@ -2131,14 +2589,29 @@ let prepareCommitLocal (fspath, magic) =
      showArchive archive;
      Format.print_flush();
    **)
-  let archiveHash = checkArchive true [] archive 0 in
+  let archiveHash =
+    if not compatMode then checkArchive true [] archive 0
+    else checkArchive251 true [] (to_compat251 archive) 0 in
   let props = getArchiveProps root in
+  let props = externArchivePropsdata archive props in
   storeArchiveLocal
     (Util.fileInUnisonDir newName) root archive archiveHash magic props;
   Lwt.return (Some archiveHash)
 
-let prepareCommitOnRoot
-   = Remote.registerRootCmd "prepareCommit" prepareCommitLocal
+let prepareCommitOnRoot =
+   Remote.registerRootCmdWithConnection "prepareCommit"
+     Umarshal.(prod2 Fspath.m string id id) Umarshal.(option int)
+     (fun conn (fspath, magic) ->
+       let compatMode = Remote.connectionVersion conn = 0 in
+       prepareCommitLocal compatMode (fspath, magic))
+
+let prepareCommitOnRoots magic =
+  match Globals.rootsInCanonicalOrder () with
+  | [(Local, _); (Local, _)] ->
+      Globals.allRootsMap (fun r -> prepareCommitLocal false (snd r, magic))
+  | [(Local, _); (Remote _, _) as r'] ->
+      Globals.allRootsMap (fun r -> prepareCommitOnRoot r r' (snd r, magic))
+  | _ -> assert false
 
 (* To really commit, first prepare (write to scratch arch.), then make sure
    the checksum on all archives are equal, finally flip scratch to main.  In
@@ -2153,7 +2626,7 @@ let commitUpdates () =
             Format.sprintf "%s\000%.0f.%d"
               ((Case.ops ())#modeDesc) (Unix.gettimeofday ()) (Unix.getpid ())
           in
-          Globals.allRootsMap (fun r -> prepareCommitOnRoot r magic)
+          prepareCommitOnRoots magic
             >>= (fun checksums ->
           if archivesIdentical checksums then begin
             (* Move scratch archives to new *)
@@ -2291,9 +2764,16 @@ let markEqualLocal fspath paths =
        archive := arch);
   setArchiveLocal root !archive
 
+let convV0 =
+  let to_compat251 = Tree.map (fun nm -> nm) Common.uc_to_compat251
+  and of_compat251 = Tree.map (fun nm -> nm) Common.uc_of_compat251 in
+  Remote.makeConvV0FunArg
+    (fun (fspath, paths) -> (fspath, to_compat251 paths))
+    (fun (fspath, paths) -> (fspath, of_compat251 paths))
+
 let markEqualOnRoot =
   Remote.registerRootCmd
-    "markEqual"
+    "markEqual" ~convV0 (Tree.m Name.m Common.mupdateContent) Umarshal.unit
     (fun (fspath, paths) -> markEqualLocal fspath paths; Lwt.return ())
 
 let markEqual equals =
@@ -2318,9 +2798,14 @@ let replaceArchiveLocal fspath path newArch =
     updatePathInArchive archive fspath Path.empty path (fun _ _ -> newArch) in
   setArchiveLocal root archive
 
+let convV0 = Remote.makeConvV0FunArg
+  (fun (fspath, (pathTo, arch)) -> (fspath, (pathTo, to_compat251 arch)))
+  (fun (fspath, (pathTo, arch)) -> (fspath, (pathTo, of_compat251 arch)))
+
 let replaceArchiveOnRoot =
   Remote.registerRootCmd
-    "replaceArchive"
+    "replaceArchive" ~convV0
+    Umarshal.(prod2 Path.m marchive id id) Umarshal.unit
     (fun (fspath, (pathTo, arch)) ->
        replaceArchiveLocal fspath pathTo arch;
        Lwt.return ())
@@ -2395,8 +2880,7 @@ let fastCheckMiss path desc ress oldDesc oldRess =
 let doMarkPossiblyUpdated arch =
   match arch with
     ArchiveFile (desc, fp, stamp, ress) ->
-      (* It would be cleaner to have a special stamp for this *)
-      ArchiveFile (desc, fp, Fileinfo.InodeStamp (-1), ress)
+      ArchiveFile (desc, fp, Fileinfo.RescanStamp, ress)
   | _ ->
       (* Should not happen, actually.  But this is hard to test... *)
       arch
@@ -2496,8 +2980,8 @@ let checkNoUpdates fspath pathInArchive ui =
   (* ...and check that this is a good description of what's out in the world *)
   let scanInfo =
     { fastCheck = false; dirFastCheck = false;
-      dirStamp = Props.changedDirStamp; archHash = "" (* Not used *);
-      showStatus = false } in
+      dirStamp = Props.changedDirStamp; rescanProps = true;
+      archHash = "" (* Not used *); showStatus = false } in
   let (_, uiNew) = buildUpdateRec archive fspath localPath scanInfo in
   markPossiblyUpdatedRec fspath pathInArchive uiNew;
   explainUpdate pathInArchive uiNew;
@@ -2595,5 +3079,5 @@ let rec iterFiles fspath path arch f =
 (* Hook for filesystem auto-detection (not implemented yet) *)
 let inspectFilesystem =
   Remote.registerRootCmd
-    "inspectFilesystem"
+    "inspectFilesystem" Umarshal.unit Proplist.m
     (fun _ -> Lwt.return Proplist.empty)
diff --git a/src/update.mli b/src/update.mli
index 0456e3f..efe8121 100644
--- a/src/update.mli
+++ b/src/update.mli
@@ -3,12 +3,23 @@
 
 module NameMap : MyMap.S with type key = Name.t
 
+type archive251 =
+    ArchiveDir of Props.t251 * archive251 NameMap.t
+  | ArchiveFile of Props.t251 * Os.fullfingerprint * Fileinfo.stamp251 * Osx.ressStamp
+  | ArchiveSymlink of string
+  | NoArchive
+
 type archive =
     ArchiveDir of Props.t * archive NameMap.t
   | ArchiveFile of Props.t * Os.fullfingerprint * Fileinfo.stamp * Osx.ressStamp
   | ArchiveSymlink of string
   | NoArchive
 
+val marchive : archive Umarshal.t
+
+val to_compat251 : archive -> archive251
+val of_compat251 : archive251 -> archive
+
 (* Calculate a canonical name for the set of roots to be synchronized.  This
    will be used in constructing the archive name for each root. Note, all
    the roots in this canonical name will contain hostnames, even local
@@ -22,9 +33,9 @@ val getRootsName : unit -> string
    paths known not to be synchronized and a list of paths not to
    check. Returns structures describing dirty files/dirs (1 per path
    given in the -path preference). An option controls whether we
-   would like to use the external filesytem monitoring process. *)
+   would like to use the external filesystem monitoring process. *)
 val findUpdates :
-  ?wantWatcher:unit ->
+  ?wantWatcher:bool ->
   (Path.t list * Path.t list) option ->
   ((Path.local * Common.updateItem * Props.t list) *
       (Path.local * Common.updateItem * Props.t list)) list
diff --git a/src/uutil.ml b/src/uutil.ml
index c5f20b7..5c0d96b 100644
--- a/src/uutil.ml
+++ b/src/uutil.ml
@@ -34,7 +34,7 @@ let myNameAndVersion = myName ^ " " ^ myVersion
 
 let hash2 x y = (17 * x + 257 * y) land 0x3FFFFFFF
 
-external hash_param : int -> int -> 'a -> int = "unsn_hash_univ_param" "noalloc"
+external hash_param : int -> int -> 'a -> int = "unsn_hash_univ_param" [@@noalloc]
 
 let hash x = hash_param 10 100 x
 
@@ -44,6 +44,7 @@ let hash x = hash_param 10 100 x
 
 module type FILESIZE = sig
   type t
+  val m : t Umarshal.t
   val zero : t
   val dummy : t
   val add : t -> t -> t
@@ -62,6 +63,7 @@ end
 
 module Filesize : FILESIZE = struct
   type t = int64
+  let m = Umarshal.int64
   let zero = 0L
   let dummy = -1L
   let add = Int64.add
@@ -89,6 +91,7 @@ end
 module File =
   struct
     type t = int
+    let m = Umarshal.int
     let dummy = -1
     let ofLine l = l
     let toLine l = assert (l <> dummy); l
diff --git a/src/uutil.mli b/src/uutil.mli
index acc2657..a2c16e7 100644
--- a/src/uutil.mli
+++ b/src/uutil.mli
@@ -18,6 +18,7 @@ val hash : 'a -> int
 
 module type FILESIZE = sig
   type t
+  val m : t Umarshal.t
   val zero : t
   val dummy : t
   val add : t -> t -> t
@@ -41,6 +42,7 @@ module Filesize : FILESIZE
 module File :
   sig
     type t
+    val m : t Umarshal.t
     val ofLine : int -> t
     val toLine : t -> int
     val toString : t -> string
diff --git a/src/xferhint.ml b/src/xferhint.ml
index c6e5dc2..876573f 100644
--- a/src/xferhint.ml
+++ b/src/xferhint.ml
@@ -20,11 +20,12 @@ let debug = Trace.debug "xferhint"
 
 let xferbycopying =
   Prefs.createBool "xferbycopying" true
-    "!optimize transfers using local copies"
+    ~category:(`Advanced `Remote)
+    "optimize transfers using local copies"
     ("When this preference is set, Unison will try to avoid transferring "
    ^ "file contents across the network by recognizing when a file with the "
    ^ "required contents already exists in the target replica.  This usually "
-   ^ "allows file moves to be propagated very quickly.  The default value is"
+   ^ "allows file moves to be propagated very quickly.  The default value is "
    ^ "\\texttt{true}.  ")
 
 module FPMap =
diff --git a/tests/fstest/fstest.1 b/tests/fstest/fstest.1
deleted file mode 100644
index 4cdb400..0000000
--- a/tests/fstest/fstest.1
+++ /dev/null
@@ -1,79 +0,0 @@
-.\"Modified from man(1) of FreeBSD, the NetBSD mdoc.template, and mdoc.samples.
-.\"See Also:
-.\"man mdoc.samples for a complete listing of options
-.\"man mdoc for the short list of editing options
-.\"/usr/share/misc/mdoc.template
-.Dd 27/06/08               \" DATE
-.Dt fstest 1      \" Program name and manual section number
-.Os Darwin
-.Sh NAME                 \" Section Header - required - don't modify
-.Nm fstest,
-.\" The following lines are read in generating the apropos(man -k) database. Use only key
-.\" words here as the database is built based on the words here and in the .ND line.
-.Nm Other_name_for_same_program(),
-.Nm Yet another name for the same program.
-.\" Use .Nm macro to designate other names for the documented program.
-.Nd This line parsed for whatis database.
-.Sh SYNOPSIS             \" Section Header - required - don't modify
-.Nm
-.Op Fl abcd              \" [-abcd]
-.Op Fl a Ar path         \" [-a path]
-.Op Ar file              \" [file]
-.Op Ar                   \" [file ...]
-.Ar arg0                 \" Underlined argument - use .Ar anywhere to underline
-arg2 ...                 \" Arguments
-.Sh DESCRIPTION          \" Section Header - required - don't modify
-Use the .Nm macro to refer to your program throughout the man page like such:
-.Nm
-Underlining is accomplished with the .Ar macro like this:
-.Ar underlined text .
-.Pp                      \" Inserts a space
-A list of items with descriptions:
-.Bl -tag -width -indent  \" Begins a tagged list
-.It item a               \" Each item preceded by .It macro
-Description of item a
-.It item b
-Description of item b
-.El                      \" Ends the list
-.Pp
-A list of flags and their descriptions:
-.Bl -tag -width -indent  \" Differs from above in tag removed
-.It Fl a                 \"-a flag as a list item
-Description of -a flag
-.It Fl b
-Description of -b flag
-.El                      \" Ends the list
-.Pp
-.\" .Sh ENVIRONMENT      \" May not be needed
-.\" .Bl -tag -width "ENV_VAR_1" -indent \" ENV_VAR_1 is width of the string ENV_VAR_1
-.\" .It Ev ENV_VAR_1
-.\" Description of ENV_VAR_1
-.\" .It Ev ENV_VAR_2
-.\" Description of ENV_VAR_2
-.\" .El
-.Sh FILES                \" File used or created by the topic of the man page
-.Bl -tag -width "/Users/joeuser/Library/really_long_file_name" -compact
-.It Pa /usr/share/file_name
-FILE_1 description
-.It Pa /Users/joeuser/Library/really_long_file_name
-FILE_2 description
-.El                      \" Ends the list
-.\" .Sh DIAGNOSTICS       \" May not be needed
-.\" .Bl -diag
-.\" .It Diagnostic Tag
-.\" Diagnostic informtion here.
-.\" .It Diagnostic Tag
-.\" Diagnostic informtion here.
-.\" .El
-.Sh SEE ALSO
-.\" List links in ascending order by section, alphabetically within a section.
-.\" Please do not reference files that do not exist without filing a bug report
-.Xr a 1 ,
-.Xr b 1 ,
-.Xr c 1 ,
-.Xr a 2 ,
-.Xr b 2 ,
-.Xr a 3 ,
-.Xr b 3
-.\" .Sh BUGS              \" Document known, unremedied bugs
-.\" .Sh HISTORY           \" Document history if command behaves in a unique manner
\ No newline at end of file
diff --git a/tests/fstest/fstest.m b/tests/fstest/fstest.m
deleted file mode 100644
index ffd1a56..0000000
--- a/tests/fstest/fstest.m
+++ /dev/null
@@ -1,72 +0,0 @@
-#import <Foundation/Foundation.h>
-#include <CoreServices/CoreServices.h>
-
-void myCallbackFunction(
-    ConstFSEventStreamRef streamRef,
-    void *clientCallBackInfo,
-    size_t numEvents,
-    void *eventPaths,
-    const FSEventStreamEventFlags eventFlags[],
-    const FSEventStreamEventId eventIds[])
-{
-    int i;
-    char **paths = eventPaths;
-
-    // printf("Callback called\n");
-    for (i=0; i<numEvents; i++) {
-        /* flags are unsigned long, IDs are uint64_t
-        printf("Change %llu in %s, flags %lu\n", eventIds[i], paths[i], eventFlags[i]); */
-                if (eventFlags[i] == kFSEventStreamEventFlagMustScanSubDirs) {
-                        printf("*%s\n",paths[i]);
-                } else {
-                        printf(".%s\n",paths[i]);
-                }
-   }
-}
-
-
-int main (int argc, const char * argv[]) {
-    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
-        int i;
-        CFMutableArrayRef pathsToWatch;
-
-        if (argc == 1) {
-                exit(0);
-        }
-
-        /* Define variables and create a CFArray object containing
-       CFString objects containing paths to watch.
-
-           There may be a simpler or nicer way to do this than using a mutable array, but I don't know it.
-     */
-
-        pathsToWatch = CFArrayCreateMutable(NULL, argc - 1, &kCFTypeArrayCallBacks);
-
-        for (i=1; i<argc; i++) {
-                CFArrayAppendValue(pathsToWatch, CFStringCreateWithCString(NULL, argv[i], kCFStringEncodingUTF8));
-        }
-
-    void *callbackInfo = NULL; // could put stream-specific data here.
-    FSEventStreamRef stream;
-    CFAbsoluteTime latency = 1.0; /* Latency in seconds */
-
-    /* Create the stream, passing in a callback */
-    stream = FSEventStreamCreate(NULL,
-        &myCallbackFunction,
-        callbackInfo,
-        pathsToWatch,
-        kFSEventStreamEventIdSinceNow, /* Or a previous event ID */
-        latency,
-        kFSEventStreamCreateFlagNone /* Flags explained in reference */
-    );
-
-    /* Create the stream before calling this. */
-    FSEventStreamScheduleWithRunLoop(stream, CFRunLoopGetCurrent(),         kCFRunLoopDefaultMode);
-
-        FSEventStreamStart(stream);
-
-        CFRunLoopRun();
-
-    [pool drain];
-    return 0;
-}
diff --git a/tests/fstest/fstest.xcodeproj/project.pbxproj b/tests/fstest/fstest.xcodeproj/project.pbxproj
deleted file mode 100644
index bbe32a7..0000000
--- a/tests/fstest/fstest.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,229 +0,0 @@
-// !$*UTF8*$!
-{
-	archiveVersion = 1;
-	classes = {
-	};
-	objectVersion = 47;
-	objects = {
-
-/* Begin PBXBuildFile section */
-		2A7DF5020E157F9600A8190E /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A7DF5010E157F9600A8190E /* CoreServices.framework */; };
-		8DD76F9A0486AA7600D96B5E /* fstest.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* fstest.m */; settings = {ATTRIBUTES = (); }; };
-		8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08FB779EFE84155DC02AAC07 /* Foundation.framework */; };
-		8DD76F9F0486AA7600D96B5E /* fstest.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = C6859EA3029092ED04C91782 /* fstest.1 */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXCopyFilesBuildPhase section */
-		8DD76F9E0486AA7600D96B5E /* CopyFiles */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 8;
-			dstPath = /usr/share/man/man1/;
-			dstSubfolderSpec = 0;
-			files = (
-				8DD76F9F0486AA7600D96B5E /* fstest.1 in CopyFiles */,
-			);
-			runOnlyForDeploymentPostprocessing = 1;
-		};
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
-		08FB7796FE84155DC02AAC07 /* fstest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = fstest.m; sourceTree = "<group>"; };
-		08FB779EFE84155DC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
-		2A7DF5010E157F9600A8190E /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = /System/Library/Frameworks/CoreServices.framework; sourceTree = "<absolute>"; };
-		32A70AAB03705E1F00C91783 /* fstest_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fstest_Prefix.pch; sourceTree = "<group>"; };
-		8DD76FA10486AA7600D96B5E /* fstest */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = fstest; sourceTree = BUILT_PRODUCTS_DIR; };
-		C6859EA3029092ED04C91782 /* fstest.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = fstest.1; sourceTree = "<group>"; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
-		8DD76F9B0486AA7600D96B5E /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				8DD76F9C0486AA7600D96B5E /* Foundation.framework in Frameworks */,
-				2A7DF5020E157F9600A8190E /* CoreServices.framework in Frameworks */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
-		08FB7794FE84155DC02AAC07 /* fstest */ = {
-			isa = PBXGroup;
-			children = (
-				08FB7795FE84155DC02AAC07 /* Source */,
-				C6859EA2029092E104C91782 /* Documentation */,
-				08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */,
-				1AB674ADFE9D54B511CA2CBB /* Products */,
-			);
-			name = fstest;
-			sourceTree = "<group>";
-		};
-		08FB7795FE84155DC02AAC07 /* Source */ = {
-			isa = PBXGroup;
-			children = (
-				32A70AAB03705E1F00C91783 /* fstest_Prefix.pch */,
-				08FB7796FE84155DC02AAC07 /* fstest.m */,
-			);
-			name = Source;
-			sourceTree = "<group>";
-		};
-		08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */ = {
-			isa = PBXGroup;
-			children = (
-				2A7DF5010E157F9600A8190E /* CoreServices.framework */,
-				08FB779EFE84155DC02AAC07 /* Foundation.framework */,
-			);
-			name = "External Frameworks and Libraries";
-			sourceTree = "<group>";
-		};
-		1AB674ADFE9D54B511CA2CBB /* Products */ = {
-			isa = PBXGroup;
-			children = (
-				8DD76FA10486AA7600D96B5E /* fstest */,
-			);
-			name = Products;
-			sourceTree = "<group>";
-		};
-		C6859EA2029092E104C91782 /* Documentation */ = {
-			isa = PBXGroup;
-			children = (
-				C6859EA3029092ED04C91782 /* fstest.1 */,
-			);
-			name = Documentation;
-			sourceTree = "<group>";
-		};
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
-		8DD76F960486AA7600D96B5E /* fstest */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "fstest" */;
-			buildPhases = (
-				8DD76F990486AA7600D96B5E /* Sources */,
-				8DD76F9B0486AA7600D96B5E /* Frameworks */,
-				8DD76F9E0486AA7600D96B5E /* CopyFiles */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-			);
-			name = fstest;
-			productInstallPath = "$(HOME)/bin";
-			productName = fstest;
-			productReference = 8DD76FA10486AA7600D96B5E /* fstest */;
-			productType = "com.apple.product-type.tool";
-		};
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
-		08FB7793FE84155DC02AAC07 /* Project object */ = {
-			isa = PBXProject;
-			attributes = {
-				LastUpgradeCheck = 0720;
-			};
-			buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "fstest" */;
-			compatibilityVersion = "Xcode 6.3";
-			developmentRegion = English;
-			hasScannedForEncodings = 1;
-			knownRegions = (
-				en,
-			);
-			mainGroup = 08FB7794FE84155DC02AAC07 /* fstest */;
-			projectDirPath = "";
-			projectRoot = "";
-			targets = (
-				8DD76F960486AA7600D96B5E /* fstest */,
-			);
-		};
-/* End PBXProject section */
-
-/* Begin PBXSourcesBuildPhase section */
-		8DD76F990486AA7600D96B5E /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				8DD76F9A0486AA7600D96B5E /* fstest.m in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
-/* End PBXSourcesBuildPhase section */
-
-/* Begin XCBuildConfiguration section */
-		1DEB927508733DD40010E9CD /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				COPY_PHASE_STRIP = NO;
-				GCC_DYNAMIC_NO_PIC = NO;
-				GCC_ENABLE_FIX_AND_CONTINUE = YES;
-				GCC_MODEL_TUNING = G5;
-				GCC_OPTIMIZATION_LEVEL = 0;
-				GCC_PRECOMPILE_PREFIX_HEADER = YES;
-				GCC_PREFIX_HEADER = fstest_Prefix.pch;
-				INSTALL_PATH = /usr/local/bin;
-				PRODUCT_NAME = fstest;
-				ZERO_LINK = YES;
-			};
-			name = Debug;
-		};
-		1DEB927608733DD40010E9CD /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
-				GCC_MODEL_TUNING = G5;
-				GCC_PRECOMPILE_PREFIX_HEADER = YES;
-				GCC_PREFIX_HEADER = fstest_Prefix.pch;
-				INSTALL_PATH = /usr/local/bin;
-				PRODUCT_NAME = fstest;
-			};
-			name = Release;
-		};
-		1DEB927908733DD40010E9CD /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				ENABLE_TESTABILITY = YES;
-				GCC_WARN_ABOUT_RETURN_TYPE = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.6;
-				ONLY_ACTIVE_ARCH = YES;
-				PREBINDING = NO;
-				SDKROOT = macosx;
-			};
-			name = Debug;
-		};
-		1DEB927A08733DD40010E9CD /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				GCC_WARN_ABOUT_RETURN_TYPE = YES;
-				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.6;
-				PREBINDING = NO;
-				SDKROOT = macosx;
-			};
-			name = Release;
-		};
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
-		1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "fstest" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				1DEB927508733DD40010E9CD /* Debug */,
-				1DEB927608733DD40010E9CD /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-		1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "fstest" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				1DEB927908733DD40010E9CD /* Debug */,
-				1DEB927A08733DD40010E9CD /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
-/* End XCConfigurationList section */
-	};
-	rootObject = 08FB7793FE84155DC02AAC07 /* Project object */;
-}
diff --git a/tests/fstest/fstest_Prefix.pch b/tests/fstest/fstest_Prefix.pch
deleted file mode 100644
index d3af66c..0000000
--- a/tests/fstest/fstest_Prefix.pch
+++ /dev/null
@@ -1,7 +0,0 @@
-//
-// Prefix header for all source files of the 'fstest' target in the 'fstest' project.
-//
-
-#ifdef __OBJC__
-    #import <Foundation/Foundation.h>
-#endif
diff --git a/unison.opam b/unison.opam
index 172c81d..bd74d2a 100644
--- a/unison.opam
+++ b/unison.opam
@@ -1,5 +1,5 @@
 opam-version: "2.0"
-maintainer: "juergen@hoetzel.info"
+maintainer: "unison-hackers@lists.seas.upenn.edu"
 authors: [
   "Trevor Jim"
   "Benjamin C. Pierce"
@@ -11,9 +11,9 @@ bug-reports: "https://github.com/bcpierce00/unison/issues"
 dev-repo: "git://github.com/bcpierce00/unison.git"
 build: ["dune" "build" "-p" name "-j" jobs]
 depends: [
-  "ocaml" {>= "4.03"}
+  "ocaml" {>= "4.08"}
   "dune" {>= "2.3"}
-  "lablgtk" {>= "2.18.6"}
+  "lablgtk3" {>= "3.1.0"}
 ]
 synopsis: "File-synchronization tool for Unix and Windows"
 description: """

More details

Full run details

Historical runs