馃悷 My personal dotfiles
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