First Commit

This commit is contained in:
root
2025-12-06 07:09:50 +07:00
commit 620fa04b79
5 changed files with 639 additions and 0 deletions

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Michael Jedich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

115
README.md Normal file
View File

@@ -0,0 +1,115 @@
# Git Repo Watcher
A simple bash script to watch a git repository and pull upstream changes if available.
### Requirements
* Bash with Version > 3
* Tested on Ubuntu, Debian, MacOS, [Windows Git Shell](https://git-scm.com/download/win), [Windows Subsystem for Linux (WSL)](https://www.microsoft.com/en-us/p/ubuntu/9nblggh4msv6)
Basically, it will work anywhere you can install Bash.
If something doesn't work, please [let me know](https://github.com/kolbasa/git-repo-watcher/issues).
### Usage
You only need the path to your git repository to start.
Make sure your local repository is tracking a remote branch, otherwise the script will fail.
This will start a watcher that looks for changes every 10 seconds:
```bash
./git-repo-watcher -d "/path/to/your/repository"
```
The time interval can be changed by passing it to `-i` (seconds):
```bash
./git-repo-watcher -d "/path/to/your/repository" -i 60
```
You can also turn off the watcher by passing `-o`.
This will execute the script only once.
```bash
./git-repo-watcher -d "/path/to/your/repository" -o
```
### Customizations
You can add your own logic to the file: [`git-repo-watcher-hooks`](https://github.com/kolbasa/git-repo-watcher/blob/master/git-repo-watcher-hooks)
For example, you can start your build process in case of changes:
```bash
# $1 - Git repository name
# $2 - Branch name
# $3 - Commit details
change_pulled() {
echo "Starting build for commit: $@"
./your-build-script.sh
}
```
If you have more than one repository you can pass a copy of the `git-repo-watcher-hooks` file like so:
```bash
./git-repo-watcher -d "/path/to/your/repository" -h "/path/to/your/hooks-file"
```
### Private repositories
The script works with private repositories.
First configure a password cache with `git config --global credential.helper "cache --timeout=60"`.
Make sure the `timeout` is greater than the time interval given to the script. Both are given as seconds.
The program will execute `git fetch` and ask for your login data. The script itself **does not** store passwords!
If you want it to run in the background as a daemon process, you have to execute `git fetch` beforehand.
Example code:
```bash
cd "/path/to/your/repository"
git config --global credential.helper "cache --timeout=60"
git fetch
# Checking exit code
if [[ $? -eq 1 ]]; then
echo "Wrong password!" >&2
exit 1
fi
# Disown process
./git-repo-watcher -d "/path/to/your/repository" > "/path/to/your/logfile.log" & disown
```
### Windows 10
The easiest way is to install [Git Shell](https://git-scm.com/download/win), which also comes with bash.
The only thing you have to consider are the file separators. The Unix format should be used here:
`C:\YourGitRepository` → `/C/YourGitRepository`
It is a little more difficult with [WSL](https://www.microsoft.com/en-us/p/ubuntu/9nblggh4msv6).
This must first be installed and configured via the Windows Store.
The file structure is also slightly different:
`C:\YourGitRepository` → `/mnt/c/YourGitRepository`
### Tests
The test suite [`git-repo-watcher-tests`](https://github.com/kolbasa/git-repo-watcher/blob/master/git-repo-watcher-tests) is using the test framework [shunit2](https://github.com/kward/shunit2), it will be downloaded automatically to your `/tmp` folder.
The script has no other dependencies and requires no internet connection.
The tests create several test git repositories in the folder: `/tmp/git-repo-watcher`.
A git user should be configured, otherwise the tests will fail.
With the following line you can check if this is the case:
```bash
git config --list
```
You can configure it as follows:
```bash
git config --global user.email "your@email.com"
git config --global user.name "Your Name"
```

181
git-repo-watcher Executable file
View File

@@ -0,0 +1,181 @@
#!/usr/bin/env bash
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
default_interval_in_seconds=10
default_hooks_file="$script_dir""/git-repo-watcher-hooks"
print_usage() {
echo ""
echo "NAME"
echo " git-repo-watcher -- keeps a git repository in sync with its origin"
echo "SYNOPSIS"
echo " git-repo-watcher -d <directory> [-h <hooks-file>] [-i <interval>] [-o]"
echo "DESCRIPTION"
echo " The following options are available:"
echo " -d The path to the git repository"
echo " -i Watch interval time in seconds (defaults to 10 seconds)"
echo " -o Run once"
echo " -h Custom hooks file"
echo ""
exit 1
}
while getopts ":d:i:h:o" options; do
case "${options}" in
d)
git_repository_dir=${OPTARG}
;;
h)
hooks_file=${OPTARG}
;;
i)
interval_in_seconds=${OPTARG}
;;
o)
run_once=true
;;
*)
print_usage
;;
esac
done
shift $((OPTIND - 1))
# Validating given git directory
if [[ -z "$git_repository_dir" ]]; then
echo -e "\nERROR: Git directory (-d) not given!" >&2
print_usage
fi
if [[ ! -d "$git_repository_dir/.git" ]] || [[ ! -r "$git_repository_dir/.git" ]]; then
echo "ERROR: Git directory (-d) not found: '$git_repository_dir/.git'!" >&2
print_usage
fi
if [[ ! -w "$git_repository_dir/.git" ]]; then
echo "ERROR: Missing write permissions for the git directory (-d): '$git_repository_dir/.git'!" >&2
print_usage
fi
# Convert to absolute file path if necessary
if [[ "$git_repository_dir" != /* ]]; then
git_repository_dir="$PWD/$git_repository_dir"
fi
# Validating given hook file
[[ -z "${hooks_file}" ]] && hooks_file="$default_hooks_file"
if [[ -f "${hooks_file}" ]] && [[ -r "${hooks_file}" ]]; then
# shellcheck source=git-repo-watcher-hooks
source "${hooks_file}"
else
echo "ERROR: Hooks (-h) file not found: '$hooks_file'" >&2
print_usage
fi
# Validating given interval
if [[ -z "$interval_in_seconds" ]]; then
interval_in_seconds="$default_interval_in_seconds"
fi
# Executes user hooks
#
# $1 - Hook name
# $2-$4 - Hook arguments
hook() {
hook_name="$1"
shift
if [[ "$(type -t "$hook_name")" == "function" ]]; then
eval "$hook_name $*"
fi
}
# Pulls commit from remote git repository
#
# $1 - Git repository name
# $2 - Branch name
pull_change() {
git pull
exit_code=$?
commit_message=$(git log -1 --pretty=format:"%h | %an | %ad | %s")
if [[ $exit_code -eq 1 ]]; then
hook "pull_failed" "$1" "$2" "$commit_message"
else
hook "change_pulled" "$1" "$2" "$(printf '%q\n' "$commit_message")"
fi
}
while true; do
cd "$git_repository_dir" || exit 1
if [[ -f ".git/index.lock" ]]; then
echo "ERROR: Git repository is locked, waiting to unlock" >&2
if [[ $run_once ]]; then
break
fi
sleep $interval_in_seconds
continue
fi
git fetch
repo_name=$(basename -s .git "$(git config --get remote.origin.url)")
previous_branch="$branch"
branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
if [[ -z $branch ]]; then
echo "ERROR: Unable to get branch" >&2
exit 1
fi
if [[ -n $previous_branch ]] && [[ "$previous_branch" != "$branch" ]]; then
hook "branch_changed" "$repo_name" "$branch" "$previous_branch"
fi
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null)"
# upstream was not configured
if [[ -z "$upstream" ]]; then
hook "upstream_not_set" "$repo_name" "$branch"
if [[ $run_once ]]; then
break
fi
sleep $interval_in_seconds
continue
fi
git_local=$(git rev-parse @)
git_remote=$(git rev-parse "$upstream")
git_base=$(git merge-base @ "$upstream")
if [[ -z $started ]]; then
started=true
hook "startup" "$repo_name" "$branch"
fi
if [[ "$git_local" == "$git_remote" ]]; then
hook "no_changes" "$repo_name" "$branch"
elif [[ "$git_local" == "$git_base" ]]; then
hook "pull_change" "$repo_name" "$branch"
elif [[ "$git_remote" == "$git_base" ]]; then
hook "local_change" "$repo_name" "$branch"
else
hook "diverged" "$repo_name" "$branch"
fi
if [[ $run_once ]]; then
break
fi
sleep $interval_in_seconds
done

55
git-repo-watcher-hooks Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# $1 - Git repository name
# $2 - Branch name
startup() {
echo "Watch started: $*"
}
# $1 - Git repository name
# $2 - Branch name
no_changes() {
printf "."
#echo "Nothing changed: $*"
}
# $1 - Git repository name
# $2 - Branch name
# $3 - Commit details
change_pulled() {
echo "Changes pulled: $*"
}
# $1 - Git repository name
# $2 - Branch name
# $3 - Commit details
pull_failed() {
echo "Pull failed --> Exiting: $*"
exit 1
}
# $1 - Git repository name
# $2 - New branch name
# $3 - Old branch name
branch_changed() {
echo "Branch changed: $*"
}
# $1 - Git repository name
# $2 - Branch name
upstream_not_set() {
echo "Upstream not set: $*"
}
# $1 - Git repository name
# $2 - Branch name
local_change() {
echo "local file changed: $*"
}
# $1 - Git repository name
# $2 - Branch name
diverged() {
echo "Diverged --> Exiting: $*"
exit 1
}

267
git-repo-watcher-tests Executable file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env bash
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
main_script="git-repo-watcher"
shunit_git="https://github.com/kward/shunit2"
# The tests use the temp folder to store the test framework
# and all generated git repositories
temp_dir="/tmp/git-repo-watcher"
stdout="$temp_dir/stdout.txt"
shunit2_dir="$temp_dir/shunit2/"
remote_repo="remote"
remote_repo_clone="remote-clone"
watched_clone="watched-clone"
test_file_name="test-file"
test_commit_message="Test commit message"
# A verbose-flag can be passed ("-v")
[[ "$1" == "-v" ]] && verbose=true && shift
# Redirecting output
if [[ "$verbose" ]]; then
exec 3>&1
exec 4>&2
else
exec 3>/dev/null
exec 4>/dev/null
fi
# Create temp directory
[[ ! -d "$temp_dir" ]] && mkdir "$temp_dir"
# Checkout shunit2 unit test framework
if [[ ! -d "$shunit2_dir" ]]; then
git clone --depth 1 "$shunit_git" "$shunit2_dir"
fi
# Prints a seperator. It helps to make the output more readable.
printSeparator() {
local sep="$1"
[[ -z "$sep" ]] && sep="-"
# shellcheck disable=SC2034
for i in {1..75}; do local x=$x"$sep"; done && echo "$x"
}
# Removes created temp files and directories
cleanupTempFiles() {
[[ -f "$stdout" ]] && rm "$stdout"
[[ -d "$temp_dir/$remote_repo" ]] && rm -rf "${temp_dir:?}/$remote_repo"
[[ -d "$temp_dir/$watched_clone" ]] && rm -rf "${temp_dir:?}/$watched_clone"
[[ -d "$temp_dir/$remote_repo_clone" ]] && rm -rf "${temp_dir:?}/$remote_repo_clone"
# I don't know why, but the following tests seem to start,
# before all temp files are deleted, so we pause a little
sleep 0.05
}
# Will be executed before each test
# https://github.com/kward/shunit2#-setupteardown
setUp() {
cd "$script_dir" || exit 1
# Create a fresh 'remote' git repository
git init --bare "$temp_dir/$remote_repo" 1>&3 2>&4
git clone "$temp_dir/$remote_repo" "$temp_dir/$remote_repo_clone" 1>&3 2>&4
pushOneFile 'README.md'
git clone "$temp_dir/$remote_repo" "$temp_dir/$watched_clone" 1>&3 2>&4
}
# Will be executed after each test
tearDown() {
cleanupTempFiles
printSeparator
return 0
}
# Commiting and pushing file to remote repository
#
# $1 - File name
# $2 - Commit message
pushOneFile() {
local file="$1"
[[ -z "$file" ]] && file="$test_file_name"
local message="$2"
[[ -z "$message" ]] && message="$test_commit_message"
cd "$temp_dir/$remote_repo_clone" || exit 1
touch "$file"
git add . 1>&3 2>&4
git commit -m "$message" 1>&3 2>&4
git push 1>&3 2>&4
}
printBashVersion() {
echo && printSeparator "#"
echo "Testing on bash-version: '$BASH_VERSION'"
printSeparator "#" && echo && printSeparator
}
# Start the main git-repo-watcher-script
#
# $1 - override options
startWatcher() {
local options=" -d $temp_dir/$watched_clone"
[[ -n "$1" ]] && options="$1"
# A short interval is used to guarantee multiple iterations
options="$options -i 0.1" # 0.1 seconds
eval "$script_dir/$main_script $options" &>$stdout &
watcher_pid=$!
}
# $1 - time to collect output
collectOutput() {
local sleep_time=1
[[ -n "$1" ]] && sleep_time="$1"
# We wait one second to collect stdout of multiple
# iterations of the main watch loop.
sleep "$sleep_time"
# Killing the watcher script
disown "$watcher_pid"
kill "$watcher_pid" 2>/dev/null
# Reading the logfile
result=$(cat "$stdout")
[[ "$verbose" ]] && echo "$result"
}
# $1 - branch name
createNewBranch() {
cd "$temp_dir/$remote_repo_clone" || exit 1
git checkout -b "$1" 1>&3 2>&4
git push --set-upstream origin "$1" 1>&3 2>&4
}
# -------------------------------------------------------------------- #
# Should print the help screen, if no arguments given
testHelpScreen() {
startWatcher " " # no arguments
collectOutput 0.1
assertContains "$result" "ERROR: Git directory (-d) not given!"
assertContains "$result" "The following options are available:"
}
# -------------------------------------------------------------------- #
testNewCommitBeforeStart() {
pushOneFile
startWatcher
collectOutput
assertContains "$result" "1 file changed"
}
# -------------------------------------------------------------------- #
testNewCommitAfterStart() {
startWatcher
pushOneFile
collectOutput
assertContains "$result" "1 file changed"
}
# -------------------------------------------------------------------- #
testPulledHook() {
pushOneFile
startWatcher
collectOutput
assertContains "$result" "Changes pulled"
}
# -------------------------------------------------------------------- #
testSpecialCharactersInCommitMessage() {
local message="!$%&/()=?{[]}#<>§|;:_,.-*+~'\"´\`"
pushOneFile "$test_file_name" "$message"
startWatcher
collectOutput
assertContains "$result" "$message"
}
# -------------------------------------------------------------------- #
testNothingChangedHook() {
startWatcher
collectOutput 0.2
assertContains "$result" "Nothing changed"
}
# -------------------------------------------------------------------- #
testStartupHook() {
startWatcher
collectOutput
assertContains "$result" "Watch started"
}
# -------------------------------------------------------------------- #
testBranchChangedHook() {
local branch="new_branch"
createNewBranch "$branch"
startWatcher
sleep 0.5
cd "$temp_dir/$watched_clone" || exit 1
git checkout -t "origin/$branch" 1>&3 2>&4
collectOutput
assertContains "$result" "Branch changed"
assertContains "$result" "$branch"
}
# -------------------------------------------------------------------- #
testNoUpstreamHook() {
startWatcher
sleep 0.5
cd "$temp_dir/$watched_clone" || exit 1
git checkout -b "new_branch" 1>&3 2>&4
collectOutput
assertContains "$result" "Upstream not set"
}
# -------------------------------------------------------------------- #
testRelativePath() {
pushOneFile
# Use a relative path for the git directory
cd "$temp_dir" || exit 1
startWatcher "-d $watched_clone"
collectOutput
assertNotContains "$result" "No such file or directory"
assertContains "$result" "Changes pulled"
}
# -------------------------------------------------------------------- #
printBashVersion
# shellcheck source=/tmp/git-repo-watcher/shunit2
. $shunit2_dir/shunit2
# -------------------------------------------------------------------- #