commit 412657861ed6d6558a4dcbd7373e0426566053cf
Author: Louis Burda <contact@sinitax.com>
Date: Wed, 16 Apr 2025 21:10:54 +0200
Add basic version based on simonthum/git-sync
Diffstat:
A | git-sync | | | 384 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | git-syncd | | | 91 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 475 insertions(+), 0 deletions(-)
diff --git a/git-sync b/git-sync
@@ -0,0 +1,384 @@
+#!/usr/bin/env bash
+#
+# git-sync
+#
+# synchronize tracking repositories
+#
+# 2012-20 by Simon Thum and contributors
+# Licensed as: CC0
+#
+# This script intends to sync via git near-automatically
+# in "tracking" repositories where a nice history is not
+# crucial, but having one at all is.
+#
+# Unlike the myriad of scripts to do just that already available,
+# it follows the KISS principle: It is small, requires nothing but
+# git and bash, but does not even try to shield you from git.
+#
+# Mode sync (default)
+#
+# Sync will likely get from you from a dull normal git repo with trivial
+# changes to an updated dull normal git repo equal to origin. No more,
+# no less. The intent is to do everything that's needed to sync
+# automatically, and resort to manual intervention as soon
+# as something non-trivial occurs. It is designed to be safe
+# in that it will likely refuse to do anything not known to
+# be safe.
+#
+# Mode check
+#
+# Check only performs the basic checks to make sure the repository
+# is in an orderly state to continue syncing, i.e. committing
+# changes, pull etc. without losing any data. When check returns
+# 0, sync can start immediately. This does not, however, indicate
+# that syncing is at all likely to succeed.
+
+# command used to auto-commit file modifications
+DEFAULT_AUTOCOMMIT_CMD="git add -u ; git commit -m \"%message\";"
+
+# command used to auto-commit all changes
+ALL_AUTOCOMMIT_CMD="git add -A ; git commit -m \"%message\";"
+
+# default commit message substituted into autocommit commands
+DEFAULT_AUTOCOMMIT_MSG="changes from $(uname -n) on $(date)"
+
+
+# AUTOCOMMIT_CMD="echo \"Please commit or stash pending changes\"; exit 1;"
+# TODO mode for stash push & pop
+
+print_usage() {
+ cat << EOF
+usage: $0 [-h] [-n] [-s] [MODE]
+
+Synchronize the current branch to a remote backup
+MODE may be either "sync" (the default) or "check", to verify that the branch is ready to sync
+
+OPTIONS:
+ -h Show this message
+ -n Commit new files even if branch.\$branch_name.syncNewFiles isn't set
+ -s Sync the branch even if branch.\$branch_name.sync isn't set
+EOF
+}
+sync_new_files_anyway="false"
+sync_anyway="false"
+
+while getopts "hns" opt ; do
+ case $opt in
+ h )
+ print_usage
+ exit 0
+ ;;
+ n )
+ sync_new_files_anyway="true"
+ ;;
+ s )
+ sync_anyway="true"
+ ;;
+ esac
+done
+shift $((OPTIND-1))
+
+#
+# utility functions, some adapted from git bash completion
+#
+
+__log_msg()
+{
+ echo git-sync: $1
+}
+
+# echo the git dir
+__gitdir()
+{
+ if [ "true" = "$(git rev-parse --is-inside-work-tree "$PWD" | head -1)" ]; then
+ git rev-parse --git-dir "$PWD" 2>/dev/null
+ fi
+}
+
+# echos repo state
+git_repo_state ()
+{
+ local g="$(__gitdir)"
+ if [ -n "$g" ]; then
+ if [ -f "$g/rebase-merge/interactive" ]; then
+ echo "REBASE-i"
+ elif [ -d "$g/rebase-merge" ]; then
+ echo "REBASE-m"
+ else
+ if [ -d "$g/rebase-apply" ]; then
+ echo "AM/REBASE"
+ elif [ -f "$g/MERGE_HEAD" ]; then
+ echo "MERGING"
+ elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
+ echo "CHERRY-PICKING"
+ elif [ -f "$g/BISECT_LOG" ]; then
+ echo "BISECTING"
+ fi
+ fi
+ if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
+ if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
+ echo "|BARE"
+ else
+ echo "|GIT_DIR"
+ fi
+ elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
+ git diff --no-ext-diff --quiet --exit-code || echo "|DIRTY"
+# if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
+# git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
+# fi
+#
+# if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
+# if [ -n "$(git ls-files --others --exclude-standard)" ]; then
+# u="%"
+# fi
+# fi
+#
+# if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
+# __git_ps1_show_upstream
+# fi
+ fi
+ else
+ echo "NOGIT"
+ fi
+}
+
+# check if we only have untouched, modified or (if configured) new files
+check_initial_file_state()
+{
+ local syncNew="$(git config --get --bool branch.$branch_name.syncNewFiles)"
+ if [[ "true" == "$syncNew" || "true" == "$sync_new_files_anyway" ]]; then
+ # allow for new files
+ if [ ! -z "$(git status --porcelain | grep -E '^[^ \?][^M\?] *')" ]; then
+ echo "NonNewOrModified"
+ fi
+ else
+ # also bail on new files
+ if [ ! -z "$(git status --porcelain | grep -E '^[^ ][^M] *')" ]; then
+ echo "NotOnlyModified"
+ fi
+ fi
+}
+
+# look for local changes
+# used to decide if autocommit should be invoked
+local_changes()
+{
+ if [ ! -z "$(git status --porcelain | grep -E '^(\?\?|[MARC] |[ MARC][MD])*')" ]; then
+ echo "LocalChanges"
+ fi
+}
+
+# determine sync state of repository, i.e. how the remote relates to our HEAD
+sync_state()
+{
+ local count="$(git rev-list --count --left-right $remote_name/$branch_name...HEAD)"
+
+ case "$count" in
+ "") # no upstream
+ echo "noUpstream"
+ false
+ ;;
+ "0 0")
+ echo "equal"
+ true
+ ;;
+ "0 "*)
+ echo "ahead"
+ true
+ ;;
+ *" 0")
+ echo "behind"
+ true
+ ;;
+ *)
+ echo "diverged"
+ true
+ ;;
+ esac
+}
+
+# exit, issue warning if not in sync
+exit_assuming_sync() {
+ if [ "equal" == "$(sync_state)" ] ; then
+ __log_msg "In sync, all fine."
+ exit 0;
+ else
+ __log_msg "Synchronization FAILED! You should definitely check your repository carefully!"
+ __log_msg "(Possibly a transient network problem? Please try again in that case.)"
+ exit 3
+ fi
+}
+
+#
+# Here git-sync actually starts
+#
+
+# first some sanity checks
+rstate="$(git_repo_state)"
+if [[ -z "$rstate" || "|DIRTY" = "$rstate" ]]; then
+ __log_msg "Preparing. Repo in $(__gitdir)"
+elif [[ "NOGIT" = "$rstate" ]] ; then
+ __log_msg "No git repository detected. Exiting."
+ exit 128 # matches git's error code
+else
+ __log_msg "Git repo state considered unsafe for sync: $(git_repo_state)"
+ exit 2
+fi
+
+# determine the current branch (thanks to stackoverflow)
+branch_name=$(git symbolic-ref -q HEAD)
+branch_name=${branch_name##refs/heads/}
+
+if [ -z "$branch_name" ] ; then
+ __log_msg "Syncing is only possible on a branch."
+ git status
+ exit 2
+fi
+
+# while at it, determine the remote to operate on
+remote_name=$(git config --get branch.$branch_name.pushRemote)
+if [ -z "$remote_name" ] ; then
+ remote_name=$(git config --get remote.pushDefault)
+fi
+if [ -z "$remote_name" ] ; then
+ remote_name=$(git config --get branch.$branch_name.remote)
+fi
+
+if [ -z "$remote_name" ] ; then
+ __log_msg "the current branch does not have a configured remote."
+ echo
+ __log_msg "Please use"
+ echo
+ __log_msg " git branch --set-upstream-to=[remote_name]/$branch_name"
+ echo
+ __log_msg "replacing [remote_name] with the name of your remote, i.e. - origin"
+ __log_msg "to set the remote tracking branch for git-sync to work"
+ exit 2
+fi
+
+# check if current branch is configured for sync
+if [[ "true" != "$(git config --get --bool branch.$branch_name.sync)" && "true" != "$sync_anyway" ]] ; then
+ echo
+ __log_msg "Please use"
+ echo
+ __log_msg " git config --bool branch.$branch_name.sync true"
+ echo
+ __log_msg "to enlist branch $branch_name for synchronization."
+ __log_msg "Branch $branch_name has to have a same-named remote branch"
+ __log_msg "for git-sync to work."
+ echo
+ __log_msg "(If you don't know what this means, you should change that"
+ __log_msg "before relying on this script. You have been warned.)"
+ echo
+ exit 1
+fi
+
+# determine mode
+if [[ -z "$1" || "$1" == "sync" ]]; then
+ mode="sync"
+elif [[ "check" == "$1" ]]; then
+ mode="check"
+else
+ __log_msg "Mode $1 not recognized"
+ exit 100
+fi
+
+__log_msg "Mode $mode"
+
+__log_msg "Using $remote_name/$branch_name"
+
+# check for intentionally unhandled file states
+if [ ! -z "$(check_initial_file_state)" ] ; then
+ __log_msg "There are changed files you should probably handle manually."
+ git status
+ exit 1
+fi
+
+# if in check mode, this is all we need to know
+if [ $mode == "check" ] ; then
+ __log_msg "check OK; sync may start."
+ exit 0
+fi
+
+# check if we have to commit local changes, if yes, do so
+if [ ! -z "$(local_changes)" ]; then
+ autocommit_cmd=""
+ config_autocommit_cmd="$(git config --get branch.$branch_name.autocommitscript)"
+
+ # discern the three ways to auto-commit
+ if [ ! -z "$config_autocommit_cmd" ]; then
+ autocommit_cmd="$config_autocommit_cmd"
+ elif [[ "true" == "$(git config --get --bool branch.$branch_name.syncNewFiles)" || "true" == "$sync_new_files_anyway" ]]; then
+ autocommit_cmd=${ALL_AUTOCOMMIT_CMD}
+ else
+ autocommit_cmd=${DEFAULT_AUTOCOMMIT_CMD}
+ fi
+
+ commit_msg="$(git config --get branch.$branch_name.syncCommitMsg)"
+ if [ "" == "$commit_msg" ]; then
+ commit_msg=${DEFAULT_AUTOCOMMIT_MSG}
+ fi
+ autocommit_cmd=$(echo "$autocommit_cmd" | sed "s/%message/$commit_msg/")
+
+ __log_msg "Committing local changes using ${autocommit_cmd}"
+ eval $autocommit_cmd
+
+ # after autocommit, we should be clean
+ rstate="$(git_repo_state)"
+ if [[ ! -z "$rstate" ]]; then
+ __log_msg "Auto-commit left uncommitted changes. Please add or remove them as desired and retry."
+ exit 1
+ fi
+fi
+
+# fetch remote to get to the current sync state
+# TODO make fetching/pushing optional
+__log_msg "Fetching from $remote_name/$branch_name"
+git fetch $remote_name $branch_name
+if [ $? != 0 ] ; then
+ __log_msg "git fetch $remote_name returned non-zero. Likely a network problem; exiting."
+ exit 3
+fi
+
+case "$(sync_state)" in
+"noUpstream")
+ __log_msg "Strange state, you're on your own. Good luck."
+ exit 2
+ ;;
+"equal")
+ exit_assuming_sync
+ ;;
+"ahead")
+ __log_msg "Pushing changes..."
+ git push $remote_name $branch_name:$branch_name
+ if [ $? == 0 ]; then
+ exit_assuming_sync
+ else
+ __log_msg "git push returned non-zero. Likely a connection failure."
+ exit 3
+ fi
+ ;;
+"behind")
+ __log_msg "We are behind, fast-forwarding..."
+ git merge --ff --ff-only $remote_name/$branch_name
+ if [ $? == 0 ]; then
+ exit_assuming_sync
+ else
+ __log_msg "git merge --ff --ff-only returned non-zero ($?). Exiting."
+ exit 2
+ fi
+ ;;
+"diverged")
+ __log_msg "We have diverged. Trying to rebase..."
+ git rebase $remote_name/$branch_name
+ if [[ $? == 0 && -z "$(git_repo_state)" && "ahead" == "$(sync_state)" ]] ; then
+ __log_msg "Rebasing went fine, pushing..."
+ git push $remote_name $branch_name:$branch_name
+ exit_assuming_sync
+ else
+ __log_msg "Rebasing failed, likely there are conflicting changes. Resolve them and finish the rebase before repeating git-sync."
+ exit 1
+ fi
+ # TODO: save master, if rebasing fails, make a branch of old master
+ ;;
+esac
diff --git a/git-syncd b/git-syncd
@@ -0,0 +1,91 @@
+#!/bin/bash
+
+GIT_SYNCD_CONFIG_DIR=${GIT_SYNCD_CONFIG_DIR:-"$HOME/.config/git-sync"}
+GIT_SYNCD_CONFIG=${GIT_SYNCD_CONFIG:-"$GIT_SYNCD_CONFIG_DIR/syncd.rc"}
+
+git_syncd_repos=()
+git_syncd_ignore=()
+
+# maximum time seconds between fetching changes and syncing
+git_syncd_default_sync_timeout=600
+declare -A git_syncd_sync_timeout
+
+# number of seconds to wait after deteced change to push
+git_syncd_default_sync_delay=300
+declare -A git_syncd_sync_delay
+
+sync() {
+ echo "starting sync.."
+ cd "$repo" && git-sync
+}
+
+watcher() {
+ repo=$1
+ excludes=()
+ sync_delay=${git_syncd_sync_delay[$repo]:-$git_syncd_default_sync_delay}
+ sync_timeout=${git_syncd_sync_delay[$repo]:-$git_syncd_default_sync_delay}
+ sync "$repo"
+ timeout=$sync_timeout
+ while true; do
+ file=$(inotifywait "$repo" -r -e modify,move,create,delete \
+ --exclude '\.git' ${excludes[@]} -t "$timeout" 2>/dev/null)
+ if [ ! -z "$file" ]; then
+ echo "inotify wake: $file"
+ timeout=$sync_delay
+ else
+ sync "$repo"
+ timeout=$sync_timeout
+ fi
+ sleep 1
+ done
+}
+
+log_prefix() {
+ repo="$1"
+ repo_name=$(basename "$repo")
+ repo_dir_name=$(basename $(dirname "$repo"))
+ while read -r line; do
+ prefix="$(date "+%Y-%m-%d %T") $repo_dir_name/$repo_name"
+ prefix="$(echo "$prefix" | tr -d "\"" | tr -d "'")"
+ echo "[$prefix] $line"
+ done
+}
+
+git_syncd_watchers=()
+kill_watchers() {
+ for pid in "${git_syncd_watchers}"; do
+ kill -9 "$pid"
+ timeout 10 wait "$pid" || notify-send "Failed to kill git-syncd watcher"
+ done
+ git_syncd_watchers=()
+}
+start_watchers() {
+ for repo in "${git_syncd_repos[@]}"; do
+ echo "watching '$repo'.."
+ watcher "$repo" 2>&1 | log_prefix "$repo" &
+ git_syncd_watchers+=($!)
+ done
+}
+
+load_config() {
+ if [ -e "$GIT_SYNCD_CONFIG" ]; then
+ echo "sourcing '$GIT_SYNCD_CONFIG'.."
+ source "$GIT_SYNCD_CONFIG"
+ fi
+}
+
+load_config
+start_watchers
+while true; do
+ file=$(inotifywait --format "%w%f" -e modify,delete,create "$GIT_SYNCD_CONFIG_DIR" 2>/dev/null)
+ if [ ! -z "$file" ]; then
+ mod_path="$(realpath "$file" 2>/dev/null)"
+ config_path="$(realpath "$GIT_SYNCD_CONFIG" 2>/dev/null)"
+ if [ "$mod_path" = "$config_path" ]; then
+ kill_watchers
+ load_config
+ start_watchers
+ fi
+ fi
+ sleep 2
+done