1+ #! /bin/bash
2+
3+ #
4+ # The git-partial-clone script
5+ # Clone a subdirectory of a github/gitlab repository
6+ #
7+ # Copyright (c) 2021 Lucero Alvarado
8+ # https://github.com/lu0/git-partial-clone
9+ #
10+
11+ CONFIG_FILE_PATH=${1}
12+
13+ git-partial-clone () {
14+ # Source config file
15+ [ ${1} ] \
16+ && get-variables-from-file ${1} \
17+ || { notif err " git-partial-clone requires a configuration file." && abort ; }
18+
19+ check-mandatory-vars " GIT_HOST REPO_NAME REPO_OWNER" || abort
20+ get-token-from-file " ${TOKEN_PATH} " GIT_TOKEN
21+
22+ # Change working directory
23+ get-clone-dir-path " ${PARENT_DIR} " " ${REPO_NAME} " CLONE_DIR || abort
24+ mkdir " ${CLONE_DIR} " && cd " ${CLONE_DIR} " || abort
25+
26+ # Add origin
27+ [ -d " ${CLONE_DIR} " /.git/ ] \
28+ && notif err " ${CLONE_DIR} is already a git directory." && abort \
29+ || git init
30+ GIT_URL=${GIT_HOST} .com/${REPO_OWNER} /${REPO_NAME}
31+ [ ${GIT_USER} ] && [ ${GIT_TOKEN} ] \
32+ && git remote add origin https://${GIT_USER} :${GIT_TOKEN} @${GIT_URL} .git \
33+ || git remote add origin https://${GIT_URL} .git
34+
35+ enable-partial-clone ${CLONE_DIR} ${REMOTE_PARTIAL_DIR}
36+ fetch-commit-history ${CLONE_DIR} ${COMMIT_DEPTH}
37+
38+ # Pull branch(es)
39+ [ ${BRANCH} ] \
40+ && { notif ok " Trying to fetch branch ${BRANCH} " && \
41+ pull-single-branch ${CLONE_DIR} ${BRANCH} ; } \
42+ || { notif warn " BRANCH not specified, pulling every branch in ${REPO_NAME} ." && \
43+ pull-all-branches ${CLONE_DIR} ; }
44+
45+ # Done
46+ [ ${REMOTE_PARTIAL_DIR} ] \
47+ && notif ok " ${REMOTE_PARTIAL_DIR} of https://${GIT_URL} was cloned into" \
48+ || notif ok " https://${GIT_URL} was cloned into"
49+ notif ok " ${CLONE_DIR} "
50+ cd - && unset-variables-from-file ${1}
51+ }
52+
53+ check-mandatory-vars () {
54+ # Returns an error if a mandatory variable is missing.
55+ # Usage: check-mandatory-vars ${STRING_OF_SPACE_SEPARATED_VAR_NAMES}
56+ local vars_arr=($1 )
57+ local count=0
58+ for var_name in " ${vars_arr[@]} " ; do
59+ local var_value=" ${! var_name} "
60+ # echo "$var_name=${var_value}"
61+ [ " ${var_value} " ] \
62+ || { notif err " $var_name is mandatory." && (( ++ count)) ; }
63+ done
64+ [[ $count -eq 0 ]] && return 0 || return 1
65+ }
66+
67+ get-token-from-file () {
68+ # Reads the contents of the token file
69+ # Usage: get-token-from-file <path to token file> <OUTPUT_VARIABLE_NAME>
70+ eval TOKEN_PATH=" ${1} " # expand quoted path
71+ MSG_NO_TOKEN=" The repository must be public in order to be cloned."
72+ MSG_TOKEN_PROVIDED=" A token was found! The repository will be cloned if you have access to it."
73+
74+ [[ -z $TOKEN_PATH ]] \
75+ && notif warn " You did not provide a token. ${MSG_NO_TOKEN} " \
76+ || { MY_TOKEN=$( cat ${TOKEN_PATH} ) && [ ${MY_TOKEN} ] \
77+ && eval " ${2} ='${MY_TOKEN} '" && notif ok " ${MSG_TOKEN_PROVIDED} " \
78+ || notif err " Could not find a token in ${TOKEN_PATH} . ${MSG_NO_TOKEN} " ; }
79+ }
80+
81+ get-clone-dir-path () {
82+ # Returns the path where the repository will be cloned.
83+ # Usage: get-clone-dir-path <parent path> <name of repository> <OUTPUT_VARIABLE_NAME>
84+ local PARENT_DIR=" ${1} "
85+ local REPO_NAME=" ${2} "
86+ [[ -z " ${PARENT_DIR} " ]] && PARENT_DIR=${PWD} && notif warn " PARENT_DIR is blank"
87+
88+ # Convert to absolute
89+ [[ " ${PARENT_DIR: 0: 1} " != " /" ]] && PARENT_DIR=${PWD} /${PARENT_DIR}
90+
91+ # Remove leading slash
92+ [[ " ${PARENT_DIR} " == * / ]] && PARENT_DIR=" ${PARENT_DIR: : -1} "
93+
94+ mkdir -p " ${PARENT_DIR} " && [ -d " ${PARENT_DIR} " ] \
95+ && eval " ${3} ='${PARENT_DIR} /${REPO_NAME} '" \
96+ && notif ok " The repository will be cloned within ${PARENT_DIR} " && return 0 \
97+ || { notif err " ${PARENT_DIR} does not exist." && return 1 ; }
98+ }
99+
100+ get-variables-from-file () {
101+ # Set the variables contained in a file of key-value pairs
102+ export $( grep --invert-match ' ^#' ${1} | xargs -d ' \n' )
103+ }
104+
105+ unset-variables-from-file () {
106+ # Removes variables contained in a file of key-value pairs
107+ unset $( grep --invert-match ' ^#' ${1} | \
108+ grep --perl-regexp --only-matching ' .*(?=\=)' | xargs)
109+ }
110+
111+ enable-partial-clone () {
112+ # Enable partial cloning if a subfolder is provided
113+ local CLONE_DIR=${1}
114+ local REMOTE_PARTIAL_DIR=${2}
115+ [ ${REMOTE_PARTIAL_DIR} ] \
116+ && git -C ${CLONE_DIR} config --local extensions.partialClone origin \
117+ && git -C ${CLONE_DIR} sparse-checkout set ${REMOTE_PARTIAL_DIR}
118+ }
119+
120+ fetch-commit-history () {
121+ # Fetch history according to the provided commit depth
122+ local CLONE_DIR=" ${1} "
123+ local COMMIT_DEPTH=" ${2} "
124+ [ ${COMMIT_DEPTH} ] && [ ${COMMIT_DEPTH} -eq ${COMMIT_DEPTH} ] \
125+ && { notif warn " Using COMMIT_DEPTH=${COMMIT_DEPTH} ." \
126+ && git -C ${CLONE_DIR} fetch --depth ${COMMIT_DEPTH} --filter=blob:none \
127+ || abort clean ; } \
128+ || { notif warn " COMMIT_DEPTH not provided, fetching all of the history." \
129+ && git -C ${CLONE_DIR} fetch --filter=blob:none \
130+ || abort clean ; }
131+ }
132+
133+ pull-single-branch () {
134+ local CLONE_DIR=${1}
135+ local BRANCH=${2}
136+ git -C ${CLONE_DIR} checkout -b $BRANCH
137+ git -C ${CLONE_DIR} pull origin $BRANCH \
138+ && git -C ${CLONE_DIR} branch --set-upstream-to=origin/$BRANCH ${BRANCH} \
139+ || abort clean
140+ }
141+
142+ pull-all-branches () {
143+ # Pull every branch in the remote
144+ # and switch to the default branch
145+ local CLONE_DIR=${1}
146+
147+ # Create empty branches
148+ N_BRANCHES=$( git -C ${CLONE_DIR} branch -r | wc -l)
149+ for (( i= 1 ; i<= $N_BRANCHES ; i++ )) ; do
150+ BRANCH_NAME_i=$( git -C ${CLONE_DIR} branch -r | \
151+ head -$i | tail -1 | sed s/" origin\/" // | xargs)
152+ git -C ${CLONE_DIR} checkout -b $BRANCH_NAME_i
153+ done
154+
155+ # Pull and track every branch
156+ for (( i= 1 ; i<= $N_BRANCHES ; i++ )) ; do
157+ CURRENT_BRANCH=$( git -C ${CLONE_DIR} branch -r | \
158+ head -$i | tail -1 | sed s/" origin\/" // | xargs)
159+ git -C ${CLONE_DIR} checkout $CURRENT_BRANCH
160+ git -C ${CLONE_DIR} pull origin $CURRENT_BRANCH
161+ git -C ${CLONE_DIR} branch --set-upstream-to=origin/$CURRENT_BRANCH ${CURRENT_BRANCH}
162+ done
163+ HEAD_BRANCH=$( git -C ${CLONE_DIR} remote show origin | \
164+ grep --perl-regexp --only-matching ' (?<=HEAD branch: ).*' )
165+ git checkout ${HEAD_BRANCH}
166+ }
167+
168+ notif () {
169+ # Usage: notif <status> <message>
170+ local info=' \033[0m'
171+ local ok=' \033[0;32m'
172+ local warn=' \033[0;33m'
173+ local err=' \033[0;31m'
174+ local STATUS=${! 1}
175+ local MSG=" ${2} "
176+ printf $STATUS " ${MSG} \n"
177+ printf ${info}
178+ }
179+
180+ abort () {
181+ notif err " Aborted."
182+ case $# in
183+ 1)
184+ notif warn " Removing empty tree in ${CLONE_DIR} "
185+ rm -rf ${CLONE_DIR} && \
186+ rmdir -p --ignore-fail-on-non-empty ${CLONE_DIR%/* }
187+ ;;
188+ esac
189+ exit
190+ }
191+
192+ git-partial-clone ${CONFIG_FILE_PATH}
193+
0 commit comments