diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-01-28 17:27:40 +0100 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-01-28 17:27:40 +0100 |
commit | 4f762c2e55ea55dc67406b54809b2fa62ef6c961 (patch) | |
tree | 99ac520e755ab4c2d416536489e240202ba15538 | |
download | gitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.tar.gz gitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.tar.zst gitmirror-4f762c2e55ea55dc67406b54809b2fa62ef6c961.zip |
Initial commit
-rwxr-xr-x | gitmirror.sh | 148 |
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 | ||
2 | set -Eeuo pipefail | ||
3 | |||
4 | msg() { | ||
5 | echo "==> $1" | ||
6 | } | ||
7 | |||
8 | read_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 | |||
33 | get_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 | |||
45 | archive_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 | |||
57 | sync_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 | |||
146 | if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then | ||
147 | sync_mirror "$@" | ||
148 | fi | ||