summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-01-28 17:27:40 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-01-28 17:27:40 +0100
commit4f762c2e55ea55dc67406b54809b2fa62ef6c961 (patch)
tree99ac520e755ab4c2d416536489e240202ba15538
downloadgitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.tar.gz
gitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.tar.zst
gitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.zip
Initial commit
-rwxr-xr-xgitmirror.sh148
1 files changed, 148 insertions, 0 deletions
diff --git a/gitmirror.sh b/gitmirror.sh
new file mode 100755
index 0000000..29503d9
--- /dev/null
+++ b/gitmirror.sh
@@ -0,0 +1,148 @@
1#!/usr/bin/env bash
2set -Eeuo pipefail
3
4msg() {
5 echo "==> $1"
6}
7
8read_branches() {
9 local prefix="refs/$1/"
10 local -n read_branches_target="$2"
11 local refname
12 local objname
13 local -i name_length
14 local -i max_length=0
15 while IFS=$'\t' read -r refname objname; do
16 local branch_name="${refname#"${prefix}"}"
17 read_branches_target["${branch_name}"]="${objname}"
18 name_length=${#branch_name}
19 if (( name_length > max_length )); then
20 max_length=$name_length
21 fi
22 done < <(git for-each-ref "${prefix}**" \
23 --format=$'%(refname)\t%(objectname)' \
24 --color=never)
25 for branch_name in "${!read_branches_target[@]}"; do
26 printf " %*s = %s\n" \
27 $((-max_length)) \
28 "${branch_name}" \
29 "${read_branches_target["${branch_name}"]}"
30 done
31}
32
33get_or_empty() {
34 local -n get_or_empty_array="$1"
35 local key="$2"
36 local -n get_or_empty_target="$3"
37 if [[ -v get_or_empty_array["${key}"] ]]; then
38 get_or_empty_target="${get_or_empty_array["${key}"]}"
39 else
40 # shellcheck disable=SC2034
41 get_or_empty_target=''
42 fi
43}
44
45archive_if_needed() {
46 local archive_prefix="$1"
47 local branch_name="$2"
48 local old_tip="$3"
49 local new_tip="$4"
50 if ! git merge-base --is-ancestor "${old_tip}" "${new_tip}"; then
51 local archive_name="${archive_prefix}${branch_name}"
52 msg "Refusing to clobber ${branch_name}, archiving as ${archive_name}"
53 git tag -a -m "Archive before synchronization" "${archive_name}" "${old_tip}"
54 fi
55}
56
57sync_mirror() {
58 if (( $# == 0 || $# > 2 )); then
59 echo "Usage: $0 REPOSITORY [REMOTE]" >&2
60 exit 1
61 fi
62
63 local repository="$1"
64 local remote
65 if (( $# == 1 )); then
66 remote="origin"
67 else
68 remote="$2"
69 fi
70 local archive_prefix
71 archive_prefix="$(TZ='UTC' printf 'archives/%(%Y%m%dT%H%M%S)TZ/')"
72
73 msg "Synchronizing ${repository} with ${remote}"
74 pushd "${repository}" > /dev/null
75
76 msg "Remote tracking branches before fetch"
77 # shellcheck disable=SC2034
78 local -A old_tracking
79 read_branches "remotes/${remote}" old_tracking
80
81 msg "Local branches"
82 # shellcheck disable=SC2034
83 local -A local_branches
84 read_branches "heads" local_branches
85
86 msg "Fetch from ${remote}"
87 git fetch --tags "${remote}"
88
89 msg "Remote tracking branches after fetch"
90 local -A new_tracking
91 read_branches "remotes/${remote}" new_tracking
92
93 local -a to_patch=()
94
95 local branch_name
96
97 for branch_name in "${!new_tracking[@]}"; do
98 local new_tip="${new_tracking["${branch_name}"]}"
99 local old_tip
100 get_or_empty old_tracking "${branch_name}" old_tip
101 local local_tip
102 get_or_empty local_branches "${branch_name}" local_tip
103 local patch_tip
104 get_or_empty local_branches "patch-for/${branch_name}" patch_tip
105
106 if [[ -n "${old_tip}" ]]; then
107 archive_if_needed "${archive_prefix}" "${remote}/${branch_name}" \
108 "${old_tip}" "${new_tip}"
109 fi
110
111 if [[ -z "${local_tip}" ]]; then
112 msg "Create local branch ${branch_name} at ${new_tip}"
113 git branch --no-track "${branch_name}" "${patch_tip:-"${new_tip}"}"
114 git branch --set-upstream-to="${remote}/${branch_name}" "${branch_name}"
115 if [[ -n "${patch_tip}" ]]; then
116 msg "Will patch newly created local branch ${branch_name}"
117 to_patch+=("${branch_name}")
118 fi
119 else
120 if [[ -z "${patch_tip}" ]]; then
121 if [[ "${local_tip}" != "${new_tip}" ]]; then
122 archive_if_needed "${archive_prefix}" "${branch_name}" \
123 "${local_tip}" "${old_tip:-"${new_tip}"}"
124 msg "Update local branch ${branch_name} to ${new_tip}"
125 git update-ref "refs/heads/${branch_name}" "${new_tip}" "${local_tip}"
126 fi
127 else
128 if ! git merge-base --is-ancestor "${new_tip}" "${local_tip}"; then
129 msg "Will update local branch ${branch_name} to ${new_tip} by patching"
130 to_patch+=("${branch_name}")
131 fi
132 fi
133 fi
134 done
135
136 local job_name="${repository}-apply-patch"
137
138 for branch_name in "${to_patch[@]}"; do
139 msg "Triggering Jenkings job ${job_name} for branch ${branch_name}"
140 # TODO
141 done
142
143 popd > /dev/null
144}
145
146if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then
147 sync_mirror "$@"
148fi