馃悷 My personal dotfiles
at main 134 lines 3.9 kB view raw
1#!/bin/bash 2# Usage: git sync 3# 4# Fetches new objects from origin remote named either "upstream" or "origin", 5# then iterates over each local branch that corresponds to a remote branch and: 6# 7# - If the local branch is outdated, fast-forward it; 8# - If the local branch contains unpushed work, warn about it; 9# - If the branch seems merged and its upstream branch was deleted, delete it. 10# 11# If a local branch doesn't have any upstream configuration, but has a 12# same-named branch on the remote, assume that's its upstream branch. 13set -e 14 15while [ $# -gt 0 ]; do 16 case "$1" in 17 -h | --help ) 18 sed -ne '/^#/!q;s/.\{1,2\}//;1d;p' < "$0" 19 exit 0 20 ;; 21 * ) 22 "$0" --help >&2 23 exit 1 24 ;; 25 esac 26 shift 1 27done 28 29GIT_DIR="$(git rev-parse --git-dir)" 30 31if [ -e "${GIT_DIR}/refs/remotes/upstream" ]; then 32 ORIGIN="upstream" 33else 34 ORIGIN="origin" 35fi 36 37if [ -t 1 ]; then 38 RED=$'\e[31m' 39 LIGHT_RED=$'\e[31;1m' 40 GREEN=$'\e[32m' 41 LIGHT_GREEN=$'\e[32;1m' 42 RESET=$'\e[0m' 43else 44 RED="" 45 LIGHT_RED="" 46 GREEN="" 47 LIGHT_GREEN="" 48 RESET="" 49fi 50 51main_branch() { 52 local remote="$1" 53 local head="$(cat "${GIT_DIR}/refs/remotes/${remote}/HEAD" 2>/dev/null)" 54 if [[ $head == "ref: "* ]]; then 55 echo "${head#ref: refs/remotes/${remote}/}" 56 else 57 return 1 58 fi 59} 60 61has_upstream_configuration() { 62 local branch="$1" 63 local remote="$(git config "branch.${branch}.remote")" 64 [ "$remote" = "$ORIGIN" ] || return 1 65} 66 67upstream_branch() { 68 local branch="$1" 69 local resolved="" 70 if resolved="$(git rev-parse --symbolic-full-name "${branch}@{upstream}" 2>/dev/null)"; then 71 echo "$resolved" 72 else 73 return 1 74 fi 75} 76 77is_ancestor() { 78 git merge-base --is-ancestor "$@" || ( 79 merge_base=$(git merge-base "$@") && [[ $(git cherry $2 $(git commit-tree $(git rev-parse $1^{tree}) -p $merge_base -m _)) == "-"* ]] 80 ) 81} 82 83git fetch "$ORIGIN" --prune --quiet --progress 84 85MASTER="$(main_branch "$ORIGIN")" || MASTER="master" 86CURRENT_BRANCH="$(git symbolic-ref --short --quiet HEAD || true)" 87 88git branch --list | \ 89 while read -r local_branch; do 90 local_branch="${local_branch#* }" 91 remote_branch="refs/remotes/${ORIGIN}/${local_branch}" 92 gone="" 93 94 if has_upstream_configuration "$local_branch"; then 95 remote_branch="$(upstream_branch "$local_branch")" || gone=1 96 elif [ ! -e "${GIT_DIR}/${remote_branch}" ]; then 97 remote_branch="" && gone=1 98 fi 99 100 if [ -n "$remote_branch" ]; then 101 shas=( `git rev-parse "$local_branch" "$remote_branch"` ) 102 local_sha="${shas[0]}" 103 remote_sha="${shas[1]}" 104 105 if [ "$local_sha" = "$remote_sha" ]; then 106 : # branch is up to date. 107 elif is_ancestor "$local_branch" "$remote_branch"; then 108 # Local branch is behind 109 if [ "$local_branch" = "$CURRENT_BRANCH" ]; then 110 git merge --ff-only --quiet "$remote_branch" 111 else 112 git update-ref "refs/heads/${local_branch}" "$remote_branch" 113 fi 114 echo "${GREEN}Updated branch ${LIGHT_GREEN}${local_branch}${RESET} (was ${local_sha:0:7})." 115 else 116 # Local branch is ahead or diverged. 117 # TODO: Decide whether to try clean merge check + rebase here. 118 echo "warning: \`$local_branch' seems to contain unpushed commits" >&2 119 fi 120 elif [ -n "$gone" ]; then 121 # Upstream branch got deleted 122 if is_ancestor "$local_branch" "${ORIGIN}/${MASTER}"; then 123 if [ "$local_branch" = "$CURRENT_BRANCH" ]; then 124 git checkout --quiet "$MASTER" 125 CURRENT_BRANCH="$MASTER" 126 fi 127 local_sha="$(git rev-parse "$local_branch")" 128 git branch -D "$local_branch" >/dev/null 129 echo "${RED}Deleted branch ${LIGHT_RED}${local_branch}${RESET} (was ${local_sha:0:7})." 130 else 131 echo "warning: \`$local_branch' is neither present on $ORIGIN nor merged into $MASTER. Have you pushed it yet?" >&2 132 fi 133 fi 134 done