Skip to content

Commit 128f118

Browse files
committed
Migrate and refactor from lu0/git-scripts/git-partial-clone to make it a submodule.
0 parents  commit 128f118

File tree

6 files changed

+977
-0
lines changed

6 files changed

+977
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
tmp/
2+
releases/
3+

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
`git-partial-clone`
2+
---
3+
4+
This script clones a subdirectory of a github/gitlab repository.
5+
6+
# Usage
7+
## Quick test
8+
Test the script with the example config file.
9+
By the end of the execution, you will see a `tmp` directory containing the subfolder of the example repository.
10+
```zsh
11+
./git-partial-clone.sh example.conf
12+
```
13+
## Install
14+
Add the script to your `PATH`
15+
```zsh
16+
ln -srf git-partial-clone.sh ~/.local/bin/git-partial-clone
17+
```
18+
Then you can execute the script from any directory with your custom config file.
19+
```zsh
20+
git-partial-clone path/to/your/config/file.conf
21+
```
22+
23+
# Configuration
24+
Fill in the config file ([`template.conf`](./template.conf)) with the information of the repository you're cloning. You can see the example file [here](./example.conf).
25+
26+
## Mandatory variables
27+
28+
- `GIT_HOST`:
29+
- `github` if the repository is hosted on Github.
30+
- `gitlab` if the repository is hosted on Gitlab.
31+
- `REPO_OWNER`:
32+
- Username of the owner/author of the repository.
33+
- `REPO_NAME`:
34+
- Name of the repository to be cloned.
35+
- **`REMOTE_PARTIAL_DIR`**:
36+
- **Subdirectory of the repository you want to clone**.
37+
- Omit it to clone the entire repository.
38+
39+
## Mandatory variables for **private repositories**
40+
You will need to generate an access token in order to clone private repositories, as password authentication is deprecated.
41+
42+
- Github: [github.com/settings/tokens](https://github.com/settings/tokens).
43+
- Gitlab: [gitlab.com/-/profile/personal_access_tokens](https://gitlab.com/-/profile/personal_access_tokens).
44+
45+
Once you have a token, store it in a file.
46+
47+
- `TOKEN_PATH`:
48+
- Path to the file containing the access token.
49+
- `GIT_USER`:
50+
- Username with access to the repository.
51+
52+
## Optional variables
53+
The following variables give you more control over the objects you're cloning.
54+
- `BRANCH`:
55+
- The branch to be fetched.
56+
- Omit it to pull all of the branches and switch to the default one.
57+
- `COMMIT_DEPTH`:
58+
- Number of commits you want to fetch (useful for deployment purposes).
59+
- Omit it to fetch the entire remote history.
60+
- `PARENT_DIR`:
61+
- Path to the target parent directory.
62+
- Omit it to clone the repository in the current directory.
63+

example.conf

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# Example of the git-partial-clone configuration file
3+
#
4+
# Copyright (c) 2021 Lucero Alvarado
5+
# https://github.com/lu0/git-partial-clone
6+
#
7+
# THIS FILE DOES NOT SUPPORT INLINE COMMENTS!
8+
#
9+
10+
GIT_HOST=github
11+
REPO_OWNER=lu0
12+
REPO_NAME=vscode-settings
13+
REMOTE_PARTIAL_DIR=json/snippets
14+
15+
TOKEN_PATH=
16+
GIT_USER=
17+
18+
BRANCH=
19+
COMMIT_DEPTH=3
20+
21+
PARENT_DIR=tmp
22+

git-partial-clone.sh

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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+

template.conf

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# Example of the git-partial-clone configuration file
3+
#
4+
# Copyright (c) 2021 Lucero Alvarado
5+
# https://github.com/lu0/git-partial-clone
6+
#
7+
# THIS FILE DOES NOT SUPPORT INLINE COMMENTS!
8+
#
9+
10+
GIT_HOST=
11+
REPO_OWNER=
12+
REPO_NAME=
13+
REMOTE_PARTIAL_DIR=
14+
15+
TOKEN_PATH=
16+
GIT_USER=
17+
18+
BRANCH=
19+
COMMIT_DEPTH=
20+
21+
PARENT_DIR=
22+

0 commit comments

Comments
 (0)