diff --git a/tools/git-map.sh b/tools/git-map.sh new file mode 100755 index 000000000000..0ea5517d7ef0 --- /dev/null +++ b/tools/git-map.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash + +set -u + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +section() { + printf '\n=== %s ===\n' "$1" +} + +val() { + [ -n "${1:-}" ] && printf '%s' "$1" || printf '%s' "-" +} + +git rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die "Not inside a Git repository" + +ROOT=$(git rev-parse --show-toplevel) +BRANCH=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || echo "DETACHED") +HEAD_COMMIT=$(git rev-parse --short=12 HEAD) + +cd "$ROOT" || exit 1 + +# ------------------------------------------------------------ +section "REPOSITORY" + +echo "path : $ROOT" +echo "branch : $BRANCH" +echo "HEAD : $HEAD_COMMIT" + +# ------------------------------------------------------------ +section "IDENTITY" + +GLOBAL_NAME=$(git config --global --get user.name 2>/dev/null || true) +GLOBAL_EMAIL=$(git config --global --get user.email 2>/dev/null || true) + +LOCAL_NAME=$(git config --local --get user.name 2>/dev/null || true) +LOCAL_EMAIL=$(git config --local --get user.email 2>/dev/null || true) + +EFFECTIVE_NAME=$(git config --get user.name 2>/dev/null || true) +EFFECTIVE_EMAIL=$(git config --get user.email 2>/dev/null || true) + +printf 'global : %s <%s>\n' "$(val "$GLOBAL_NAME")" "$(val "$GLOBAL_EMAIL")" +printf 'local : %s <%s>\n' "$(val "$LOCAL_NAME")" "$(val "$LOCAL_EMAIL")" +printf 'effective : %s <%s>\n' "$(val "$EFFECTIVE_NAME")" "$(val "$EFFECTIVE_EMAIL")" + +# ------------------------------------------------------------ +section "REMOTES" + +for remote in $(git remote); do + FETCH_URL=$(git remote get-url "$remote" 2>/dev/null || true) + PUSH_URL=$(git remote get-url --push "$remote" 2>/dev/null || true) + + echo + echo "[$remote]" + echo " fetch : $(val "$FETCH_URL")" + echo " push : $(val "$PUSH_URL")" +done + +# ------------------------------------------------------------ +section "SUBMODULES" + +if [ ! -f .gitmodules ]; then + echo "(none)" +else + git config -f .gitmodules \ + --get-regexp '^submodule\..*\.path$' 2>/dev/null | + while read -r key path; do + + name=${key#submodule.} + name=${name%.path} + + declared=$( + git config -f .gitmodules \ + --get "submodule.$name.url" 2>/dev/null || true + ) + + resolved=$( + git config \ + --get "submodule.$name.url" 2>/dev/null || true + ) + + commit=$( + git ls-tree HEAD "$path" 2>/dev/null | + awk '{print $3}' + ) + + echo + echo "[$name]" + echo " path : $path" + echo " declared : $(val "$declared")" + echo " resolved : $(val "$resolved")" + echo " commit : $(val "$commit")" + done +fi + +# ------------------------------------------------------------ +section "SSH ALIASES" + +SSH_CONFIG="$HOME/.ssh/config" + +if [ ! -f "$SSH_CONFIG" ]; then + echo "(no ~/.ssh/config)" +else + awk ' + /^Host[ \t]+/ { + host=$0 + sub(/^[ \t]*Host[ \t]+/, "", host) + print "" + print "[" host "]" + next + } + + /^[ \t]*(HostName|User|Port|IdentityFile|IdentitiesOnly)[ \t]+/ { + key=$1 + value=$2 + printf " %-15s %s\n", key ":", value + } + ' "$SSH_CONFIG" +fi + +# ------------------------------------------------------------ +section "SSH BY REMOTE" + +for remote in $(git remote); do + + url=$(git remote get-url "$remote" 2>/dev/null || true) + + case "$url" in + ssh://*) + ;; + *@*:*) + ;; + *) + echo + echo "[$remote]" + echo " protocol : non-SSH" + continue + ;; + esac + + # Extract SSH destination from URL. + if [[ "$url" =~ ^ssh://([^/@]+@)?([^/:]+)(:([0-9]+))?/.*$ ]]; then + url_user=${BASH_REMATCH[1]%@} + url_host=${BASH_REMATCH[2]} + url_port=${BASH_REMATCH[4]} + elif [[ "$url" =~ ^([^@]+)@([^:]+):.*$ ]]; then + url_user=${BASH_REMATCH[1]} + url_host=${BASH_REMATCH[2]} + url_port="" + else + echo + echo "[$remote]" + echo " unable to parse: $url" + continue + fi + + SSH_OUTPUT=$(ssh -G "$url_host" 2>/dev/null || true) + + cfg_user=$(printf '%s\n' "$SSH_OUTPUT" | + awk '$1=="user" {print $2; exit}') + + cfg_host=$(printf '%s\n' "$SSH_OUTPUT" | + awk '$1=="hostname" {print $2; exit}') + + cfg_port=$(printf '%s\n' "$SSH_OUTPUT" | + awk '$1=="port" {print $2; exit}') + + cfg_keys=$(printf '%s\n' "$SSH_OUTPUT" | + awk '$1=="identityfile" {print $2}') + + effective_user=${url_user:-$cfg_user} + effective_port=${url_port:-$cfg_port} + + echo + echo "[$remote]" + echo " URL : $url" + echo " URL user : $(val "$url_user")" + echo " URL host : $(val "$url_host")" + echo " URL port : $(val "$url_port")" + echo + echo " SSH cfg user : $(val "$cfg_user")" + echo " SSH cfg host : $(val "$cfg_host")" + echo " SSH cfg port : $(val "$cfg_port")" + + while read -r key; do + [ -n "$key" ] && echo " identity : $key" + done <<< "$cfg_keys" + + echo + echo " effective user: $(val "$effective_user")" + echo " effective host: $(val "$cfg_host")" + echo " effective port: $(val "$effective_port")" + + if [ -n "$url_user" ] && + [ -n "$cfg_user" ] && + [ "$url_user" != "$cfg_user" ]; then + echo " NOTICE : URL user overrides SSH config user" + fi + + if [ -n "$url_port" ] && + [ -n "$cfg_port" ] && + [ "$url_port" != "$cfg_port" ]; then + echo " NOTICE : URL port overrides SSH config port" + fi +done + +# ------------------------------------------------------------ +section "SSH AGENT" + +if ssh-add -l >/dev/null 2>&1; then + ssh-add -l +else + echo "INFO: no SSH agent available or no key loaded" +fi + +# ------------------------------------------------------------ +section "OBSERVATIONS" + +OBS=0 + +if [ -n "$LOCAL_NAME$LOCAL_EMAIL" ] && + { [ "$LOCAL_NAME" != "$GLOBAL_NAME" ] || + [ "$LOCAL_EMAIL" != "$GLOBAL_EMAIL" ]; }; then + + echo "NOTICE: local Git identity overrides global identity" + OBS=$((OBS + 1)) +fi + +if [ -f .gitmodules ]; then + while read -r key url; do + case "$url" in + ../*|./*) + echo "INFO: relative submodule URL: $key -> $url" + OBS=$((OBS + 1)) + ;; + esac + done < <( + git config -f .gitmodules \ + --get-regexp '^submodule\..*\.url$' 2>/dev/null + ) +fi + +if [ "$OBS" -eq 0 ]; then + echo "No notable configuration difference detected." +fi + +echo diff --git a/tools/upstream/git_divergence.sh b/tools/upstream/git_divergence.sh new file mode 100755 index 000000000000..50d39be56b70 --- /dev/null +++ b/tools/upstream/git_divergence.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +ODF_BRANCH="${1:-main}" +UPSTREAM_BRANCH="${2:-upstream/master}" + +git rev-parse --git-dir >/dev/null 2>&1 || { + echo "ERROR: not inside a Git repository" >&2 + exit 1 +} + +git rev-parse --verify "$ODF_BRANCH" >/dev/null +git rev-parse --verify "$UPSTREAM_BRANCH" >/dev/null + +BASE="$(git merge-base "$ODF_BRANCH" "$UPSTREAM_BRANCH")" +ODF_HEAD="$(git rev-parse "$ODF_BRANCH")" +UPSTREAM_HEAD="$(git rev-parse "$UPSTREAM_BRANCH")" + +TMPDIR_WORK="$(mktemp -d)" +trap 'rm -rf "$TMPDIR_WORK"' EXIT + +ODF_FILES="$TMPDIR_WORK/odfstudio-files.txt" +UPSTREAM_FILES="$TMPDIR_WORK/upstream-files.txt" +SHARED_FILES="$TMPDIR_WORK/shared-files.txt" + +git diff --name-only "$BASE".."$ODF_BRANCH" | sort -u > "$ODF_FILES" +git diff --name-only "$BASE".."$UPSTREAM_BRANCH" | sort -u > "$UPSTREAM_FILES" + +comm -12 "$ODF_FILES" "$UPSTREAM_FILES" > "$SHARED_FILES" + +ODF_COMMITS="$(git rev-list --count "$UPSTREAM_BRANCH".."$ODF_BRANCH")" +UPSTREAM_COMMITS="$(git rev-list --count "$ODF_BRANCH".."$UPSTREAM_BRANCH")" + +ODF_FILE_COUNT="$(wc -l < "$ODF_FILES")" +UPSTREAM_FILE_COUNT="$(wc -l < "$UPSTREAM_FILES")" +SHARED_FILE_COUNT="$(wc -l < "$SHARED_FILES")" + +echo "ODFStudio upstream synchronization report" +echo "=========================================" +echo +printf "BASE %s\n" "$BASE" +printf "ODFStudio %s (%s)\n" "$ODF_HEAD" "$ODF_BRANCH" +printf "Upstream %s (%s)\n" "$UPSTREAM_HEAD" "$UPSTREAM_BRANCH" + +echo +echo "Commits" +printf " ODFStudio %8d\n" "$ODF_COMMITS" +printf " Upstream %8d\n" "$UPSTREAM_COMMITS" + +echo +echo "Modified files since common base" +printf " ODFStudio %8d\n" "$ODF_FILE_COUNT" +printf " Upstream %8d\n" "$UPSTREAM_FILE_COUNT" +printf " Shared %8d\n" "$SHARED_FILE_COUNT" + +echo +echo "Upstream activity on shared files" +echo "---------------------------------" + +if [[ ! -s "$SHARED_FILES" ]]; then + echo "No shared modified files." + exit 0 +fi + +while IFS= read -r file; do + count="$(git rev-list --count "$BASE".."$UPSTREAM_BRANCH" -- "$file")" + printf "%5d %s\n" "$count" "$file" +done < "$SHARED_FILES" | sort -nr diff --git a/tools/upstream/sync-status.sh b/tools/upstream/sync-status.sh new file mode 100755 index 000000000000..b98ad9f8d574 --- /dev/null +++ b/tools/upstream/sync-status.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +ODF_BRANCH="${1:-main}" +UPSTREAM_BRANCH="${2:-upstream/master}" + +git rev-parse --git-dir >/dev/null 2>&1 || { + echo "ERROR: not inside a Git repository" >&2 + exit 1 +} + +echo "ODFStudio upstream sync status" +echo "==============================" +echo + +printf "Repository : %s\n" "$(git rev-parse --show-toplevel)" +printf "Branch : %s\n" "$(git branch --show-current)" +printf "HEAD : %s\n" "$(git rev-parse --short=12 HEAD)" +printf "ODFStudio : %s\n" "$(git rev-parse --short=12 "$ODF_BRANCH")" +printf "Upstream : %s\n" "$(git rev-parse --short=12 "$UPSTREAM_BRANCH")" +printf "Base : %s\n" "$(git merge-base "$ODF_BRANCH" "$UPSTREAM_BRANCH" | cut -c1-12)" + +echo +echo "Working tree" +echo "------------" + +if git diff --quiet && git diff --cached --quiet; then + echo "Tracked files : clean" +else + echo "Tracked files : MODIFIED" + git status --short +fi + +UNTRACKED="$(git ls-files --others --exclude-standard)" + +if [[ -z "$UNTRACKED" ]]; then + echo "Untracked : none" +else + echo "Untracked :" + printf '%s\n' "$UNTRACKED" | sed 's/^/ /' +fi + +echo +echo "Remotes" +echo "-------" +git remote -v | awk '$3 == "(fetch)" {print}' + +echo +echo "Divergence" +echo "----------" + +printf "ODFStudio-only commits : " +git rev-list --count "$UPSTREAM_BRANCH".."$ODF_BRANCH" + +printf "Upstream-only commits : " +git rev-list --count "$ODF_BRANCH".."$UPSTREAM_BRANCH" + +echo +echo "Sync readiness" +echo "--------------" + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "NOT READY: tracked working tree is modified." + exit 1 +fi + +echo "READY: tracked working tree is clean."