├── .gitignore ├── LICENSE ├── README.zh-TW.md ├── README.md └── scrabble /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Denny Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.zh-TW.md: -------------------------------------------------------------------------------- 1 | # scrabble 2 | 3 | 一個簡單的工具,用於從遠端伺服器上暴露的 `.git` 資料夾中恢復 Git 儲存庫。 4 | 5 | ## 快速開始 6 | 7 | 1. **下載腳本:** 8 | ```bash 9 | curl -O https://raw.githubusercontent.com/denny0223/scrabble/refs/heads/master/scrabble 10 | ``` 11 | 12 | 2. **賦予執行權限:** 13 | ```bash 14 | chmod +x scrabble 15 | ``` 16 | 17 | 3. **執行腳本:** 18 | ```bash 19 | ./scrabble [directory] 20 | ``` 21 | 22 | ## 使用方式 23 | 24 | `scrabble [directory]` 25 | 26 | ### 參數: 27 | 28 | * ``: 儲存庫 `.git` 資料夾的完整 URL (例如:`http://example.com/my-project.git/`)。 29 | * `[directory]` (選填): 要將儲存庫複製到的本地資料夾。 30 | * 如果未提供,腳本將預設使用從 URL 派生的儲存庫名稱作為資料夾名稱 (例如:從 `http://example.com/my-project.git/` 派生出 `my-project`)。 31 | 32 | ### 重要注意事項: 33 | 34 | * 您需要確保目標 URL 具有暴露的 `.git` 資料夾。 35 | * 本腳本**不會**覆蓋現有資料夾。如果目標資料夾已存在,腳本將會報錯並終止執行,以防止意外的資料遺失。請移除現有資料夾或選擇不同的名稱。 36 | * 此工具旨在將儲存庫複製到一個**新的、空的資料夾**中。請勿在現有的 Git 儲存庫中執行此工具,否則它將覆蓋其內容。 37 | 38 | ## 範例 39 | 40 | ```bash 41 | # 複製到名為 'my-project' 的資料夾 42 | scrabble http://example.com/my-project.git/ 43 | 44 | # 複製到名為 'my-local-repo' 的特定資料夾 45 | scrabble http://example.com/my-project.git/ my-local-repo 46 | ``` 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrabble 2 | 3 | A simple tool to recover Git repositories from exposed `.git` folders on remote servers. 4 | 5 | ## Quick Start 6 | 7 | 1. **Download the script:** 8 | ```bash 9 | curl -O https://raw.githubusercontent.com/denny0223/scrabble/refs/heads/master/scrabble 10 | ``` 11 | 12 | 2. **Make it executable:** 13 | ```bash 14 | chmod +x scrabble 15 | ``` 16 | 17 | 3. **Run the script:** 18 | ```bash 19 | ./scrabble [directory] 20 | ``` 21 | 22 | ## Usage 23 | 24 | `scrabble [directory]` 25 | 26 | ### Arguments: 27 | 28 | * ``: The full URL to the repository's `.git` directory (e.g., `http://example.com/my-project.git/`). 29 | * `[directory]` (Optional): The local directory to clone the repository into. 30 | * If not provided, the script defaults to a directory name derived from the URL (e.g., `my-project` from `http://example.com/my-project.git/`). 31 | 32 | ### Important Notes: 33 | 34 | * You need to make sure the target URL has an exposed `.git` folder. 35 | * The script will **not** overwrite an existing directory. If the target directory already exists, the script will exit with an error to prevent accidental data loss. Please remove the existing directory or choose a different name. 36 | * This tool is designed to clone a repository into a **new, empty directory**. Do not run it inside an existing Git repository, as it will overwrite its contents. 37 | 38 | ## Example 39 | 40 | ```bash 41 | # Clone to a directory named 'my-project' 42 | scrabble http://example.com/my-project.git/ 43 | 44 | # Clone to a specific directory named 'my-local-repo' 45 | scrabble http://example.com/my-project.git/ my-local-repo 46 | ``` 47 | -------------------------------------------------------------------------------- /scrabble: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Author: Denny Huang 4 | # Wed Sep 17 2014 5 | # MIT License 6 | # 7 | # Description: 8 | # A script to recursively download git objects from a web server 9 | # that has an exposed .git directory. It reconstructs the git history locally. 10 | 11 | # Exit immediately if a command exits with a non-zero status. 12 | # Treat unset variables as an error when substituting. 13 | # Pipelines return the exit status of the last command to exit with a 14 | # non-zero status, or zero if no command exited with a non-zero status. 15 | set -o errexit -o nounset -o pipefail 16 | 17 | # --- Functions --- 18 | 19 | # Prints usage information and exits. 20 | usage() { 21 | echo "Usage: scrabble [directory]" >&2 22 | echo "" >&2 23 | echo "Arguments:" >&2 24 | echo " url The full URL to the repository's .git directory." >&2 25 | echo " directory (Optional) The directory to clone into." >&2 26 | echo " If not provided, it defaults to the repository name from the URL." >&2 27 | echo "" >&2 28 | echo "Example: scrabble http://example.com/my-project.git/" >&2 29 | echo " scrabble http://example.com/my-project.git/ my-new-folder" >&2 30 | exit 1 31 | } 32 | 33 | # Downloads a git object blob from the remote server. 34 | # Arguments: 35 | # $1: The object hash. 36 | # $2: The domain URL. 37 | downloadBlob() { 38 | local hash="$1" 39 | local domain="$2" 40 | local dir="${hash:0:2}" 41 | local file="${hash:2}" 42 | 43 | echo "Downloading blob: ${hash}" 44 | 45 | mkdir -p "$dir" 46 | # Using a subshell for cd is safer as it doesn't change the CWD of the script. 47 | ( 48 | cd "$dir" 49 | wget -q -nc "${domain}/.git/objects/${dir}/${file}" 50 | ) 51 | } 52 | 53 | # Recursively parses a git tree object. 54 | # Arguments: 55 | # $1: The tree hash. 56 | # $2: The domain URL. 57 | parseTree() { 58 | local tree_hash="$1" 59 | local domain="$2" 60 | 61 | echo "Parsing tree: ${tree_hash}" 62 | 63 | downloadBlob "$tree_hash" "$domain" 64 | 65 | # Use process substitution and a while-read loop to parse the tree entries. 66 | # This is more efficient than multiple awk/sed calls. 67 | while read -r _ type hash _; do 68 | if [[ "$type" == "tree" ]]; then 69 | parseTree "$hash" "$domain" 70 | elif [[ "$type" == "blob" ]]; then 71 | downloadBlob "$hash" "$domain" 72 | fi 73 | done < <(git cat-file -p "$tree_hash") 74 | } 75 | 76 | # Recursively parses a git commit object. 77 | # Arguments: 78 | # $1: The commit hash. 79 | # $2: The domain URL. 80 | parseCommit() { 81 | local commit_hash="$1" 82 | local domain="$2" 83 | 84 | echo "Parsing commit: ${commit_hash}" 85 | 86 | downloadBlob "$commit_hash" "$domain" 87 | 88 | # Extract tree and parent hashes more robustly. 89 | local tree 90 | tree=$(git cat-file -p "$commit_hash" | awk '/^tree/ {print $2}') 91 | parseTree "$tree" "$domain" 92 | 93 | # A commit can have multiple parents (merge commits). We must parse all of them. 94 | git cat-file -p "$commit_hash" | awk '/^parent/ {print $2}' | while read -r parent; do 95 | if [[ -n "$parent" && "${#parent}" -eq 40 ]]; then 96 | # To avoid infinite loops on repositories with circular references (which are possible), 97 | # we can add a check here to not re-parse a commit. 98 | # For now, we assume a clean history. 99 | parseCommit "$parent" "$domain" 100 | fi 101 | done 102 | } 103 | 104 | # --- Main Logic --- 105 | 106 | main() { 107 | # Check for required tools 108 | for cmd in git curl wget; do 109 | if ! command -v "$cmd" &> /dev/null; then 110 | echo "Error: Required command '$cmd' not found." >&2 111 | exit 1 112 | fi 113 | done 114 | 115 | # Check for URL argument 116 | if [[ $# -eq 0 ]]; then 117 | usage 118 | fi 119 | 120 | local domain="$1" 121 | # Ensure domain ends with a slash 122 | if [[ "${domain: -1}" != "/" ]]; then 123 | domain+="/" 124 | fi 125 | 126 | # Determine the output directory. 127 | local output_dir 128 | if [[ -n "${2:-}" ]]; then 129 | output_dir="$2" 130 | else 131 | # Default to the basename of the URL, removing .git if present. 132 | output_dir=$(basename "${domain}" .git) 133 | fi 134 | 135 | # Security check: ensure the target directory does not exist. 136 | if [[ -e "$output_dir" ]]; then 137 | echo "Error: Target directory '${output_dir}' already exists." >&2 138 | echo "Please remove it or choose a different directory." >&2 139 | exit 1 140 | fi 141 | 142 | echo "Creating target directory: ${output_dir}" 143 | mkdir -p "$output_dir" 144 | cd "$output_dir" 145 | 146 | echo "Initializing local repository..." 147 | git init -q 148 | 149 | # Fetch HEAD to find the current branch reference 150 | echo "Fetching remote HEAD..." 151 | local ref 152 | ref=$(curl -s "${domain}.git/HEAD" | awk '{print $2}') 153 | if [[ -z "$ref" ]]; then 154 | echo "Error: Could not determine remote HEAD reference." >&2 155 | echo "Is the .git directory accessible at ${domain}.git/ ?" >&2 156 | exit 1 157 | fi 158 | 159 | # Fetch the commit hash from the reference 160 | echo "Fetching commit hash for ref: $ref..." 161 | local lastHash 162 | lastHash=$(curl -s "${domain}.git/${ref}") 163 | if ! [[ "$lastHash" =~ ^[0-9a-f]{40}$ ]]; then 164 | echo "Error: Could not get a valid commit hash from ${domain}.git/${ref}" >&2 165 | exit 1 166 | fi 167 | 168 | echo "Starting download with initial commit: $lastHash" 169 | 170 | # Change to objects directory to download files. 171 | # Using a subshell is safer as it doesn't change the CWD of the main script. 172 | ( 173 | cd .git/objects/ 174 | parseCommit "$lastHash" "$domain" 175 | ) 176 | 177 | echo "Download complete." 178 | 179 | # Set the local branch to point to the downloaded commit. 180 | # This uses the ref name from the remote. 181 | echo "Updating ref '${ref}' to point to ${lastHash}" 182 | echo "$lastHash" > ".git/${ref}" 183 | 184 | # Point HEAD to the just-created ref to make it the current branch. 185 | echo "Setting HEAD to point to '${ref}'" 186 | git symbolic-ref HEAD "${ref}" 187 | 188 | # Reset the working directory to the state of the downloaded commit. 189 | echo "Resetting repository to the downloaded state..." 190 | git reset --hard 191 | echo "Done. Repository downloaded to '${output_dir}'" 192 | } 193 | 194 | # Execute the main function with all script arguments 195 | main "$@" --------------------------------------------------------------------------------