diff --git a/.gsuconf b/.gsuconf new file mode 100644 index 0000000..f27a1c3 --- /dev/null +++ b/.gsuconf @@ -0,0 +1,10 @@ +# .gsuconf — configuration for git-standup +# Lines starting with # are comments. +# Use `exclude=` or a leading `-` to exclude projects. +# Use `include=` or a leading `+` to include projects. +# This file excludes the current project directory from scans. +# Example usages: +# include=/path/to/your/project +# exclude=/path/to/your/project +# +/path/to/your/project +# -/path/to/your/project diff --git a/README.md b/README.md index 6b552d7..b37b80d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ git standup [-a ] [-r] [-c] [-R] + [-i] ``` Here is the detail for each of the options @@ -78,6 +79,7 @@ Here is the detail for each of the options | s | Silences the no activity message (useful when running in a directory having many repositories) | | c | Show diff-stat for every matched commit | r | Generates the standup report file `git-standup-report.txt` in the current directory | +| i | Ignore the local `.gsuconf` configuration file (do not apply includes/excludes) | | R | Display the author date instead of the committer date | For the basic usage, all you have to do is run `git standup` in a repository or a folder containing multiple repositories @@ -221,6 +223,26 @@ project-a project-b ``` +## Local configuration (`.gsuconf`) + +You can specify projects to explicitly include or exclude by creating a `.gsuconf` file in the directory where you run `git-standup`. + +- `include=/path/to/project` or `+relative/path` — include a project +- `exclude=/path/to/project` or `-relative/path` — exclude a project +- Lines starting with `#` are comments; whitespace is trimmed and empty entries are ignored + +If the same path (after normalization) appears in both include and exclude, `git-standup` will print a warning and **exclude takes precedence**. + +Example: + +``` ++projects/frontend +exclude=/home/me/projects/old-repo +# Comment +``` + +You can disable reading `.gsuconf` with `git standup -i`. + ## Changing the Weekdays By default, it considers that the work week starts on Monday and ends on Friday. So if you are running this on any day between Tuesday and Friday, it will show you your commits from the last day. However, if you are running this on Monday, it will show you all your commits since Friday. diff --git a/git-standup b/git-standup index bc198e3..fe18c00 100755 --- a/git-standup +++ b/git-standup @@ -25,6 +25,7 @@ Usage: -A - List commits after this date -B - List commits before this date -R - Display the author date instead of the committer date + -i - Ignore the local .gsuconf configuration file (do not apply includes/excludes) Examples: git standup -a "John Doe" -w "MON-FRI" -m 3 @@ -124,9 +125,9 @@ function runStandup() { fi } -while getopts "hgfsd:u:a:w:m:D:A:B:LrcRFb:" opt; do +while getopts "higfsd:u:a:w:m:D:A:B:LrcRFb:" opt; do case $opt in - h | d | u | a | w | m | g | D | f | s | L | r | A | B | c | R | F | b) + h | i | d | u | a | w | m | g | D | f | s | L | r | A | B | c | R | F | b) declare "option_$opt=${OPTARG:-0}" ;; \?) @@ -170,11 +171,97 @@ RAN_FROM_DIR=$(pwd) REPORT_FILE_PATH="${RAN_FROM_DIR}/git-standup-report.txt" STAT= +# Portable absolute path resolver +abspath() { + if command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then + readlink -f "$1" + elif command -v realpath >/dev/null 2>&1; then + realpath "$1" + else + + if [ -d "$1" ]; then + (cd "$1" 2>/dev/null && pwd -P) || echo "" + else + dir=$(dirname -- "$1") + base=$(basename -- "$1") + if (cd "$dir" 2>/dev/null && printf '%s\n' "$(pwd -P)/$base"); then + : + else + echo "" + fi + fi + fi +} + # If report is to be generated, remove the existing report file if any if [[ -n $option_r ]]; then rm -rf "${REPORT_FILE_PATH}" fi +# Parse .gsuconf in the current directory (optional) +# Format supported (per line): +# include=/path/to/project or +relative/path +# exclude=/path/to/project or -relative/path +# Lines starting with # are comments. +GSU_CONF_FILE="${RAN_FROM_DIR}/.gsuconf" +GSU_INCLUDES=() +GSU_EXCLUDES=() + +# If -i flag is provided, ignore the local .gsuconf entirely +if [[ ${option_i:=} ]]; then + if [[ -f "$GSU_CONF_FILE" ]]; then + echo "Ignoring $(basename "$GSU_CONF_FILE") due to -i flag" >&2 + fi +else + if [[ -f "$GSU_CONF_FILE" ]]; then + while IFS= read -r line || [[ -n $line ]]; do + # ignore comments (lines starting with #) + [[ "$line" =~ ^[[:space:]]*# ]] && continue + # trim leading/trailing whitespace (spaces, tabs, etc.) + line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [[ -z "$line" ]] && continue + if [[ "$line" == include=* ]]; then + val="${line#include=}" + [[ -n "$val" ]] && GSU_INCLUDES+=("$val") + continue + fi + if [[ "$line" == exclude=* ]]; then + val="${line#exclude=}" + [[ -n "$val" ]] && GSU_EXCLUDES+=("$val") + continue + fi + case "$line" in + +*) val="${line#+}"; [[ -n "$val" ]] && GSU_INCLUDES+=("$val") ;; + -*) val="${line#-}"; [[ -n "$val" ]] && GSU_EXCLUDES+=("$val") ;; + *) ;; + esac + done < "$GSU_CONF_FILE" + fi +fi + +# Check for include/exclude conflicts: normalize entries and warn if the same path +if [[ ${#GSU_INCLUDES[@]} -gt 0 && ${#GSU_EXCLUDES[@]} -gt 0 ]]; then + declare -a new_includes=() + for inc in "${GSU_INCLUDES[@]}"; do + [[ "$inc" = /* ]] || inc="${RAN_FROM_DIR%/}/$inc" + inc="${inc%/.git}" + inc_abs=$(abspath "$inc" 2>/dev/null || echo "") + conflict=0 + for excl in "${GSU_EXCLUDES[@]}"; do + [[ "$excl" = /* ]] || excl="${RAN_FROM_DIR%/}/$excl" + excl="${excl%/.git}" + excl_abs=$(abspath "$excl" 2>/dev/null || echo "") + if [[ -n "$inc_abs" && -n "$excl_abs" && "$inc_abs" == "$excl_abs" ]]; then + echo "${YELLOW}Warning: '$inc_abs' is present in both include and exclude; exclude will take precedence${NORMAL}" >&2 + conflict=1 + break + fi + done + [[ $conflict -eq 0 ]] && new_includes+=("$inc") + done + GSU_INCLUDES=("${new_includes[@]}") +fi + if [[ ${option_m:=} ]]; then MAXDEPTH="$((${option_m:=} + 1))" fi @@ -220,26 +307,95 @@ fi GIT_DATE_FORMAT=${option_D:-relative} # For when the command has been run in a non-repo directory +declare -a PROJECT_DIRS_ARRAY if [[ ${option_F:=} || ! -d ".git" || -f ".git" ]]; then BASE_DIR=$(pwd) # Set delimiter to newline for the loop IFS=$'\n' if [[ -f ".git-standup-whitelist" ]]; then - SEARCH_PATH=$(cat .git-standup-whitelist) + mapfile -t SEARCH_ARRAY < .git-standup-whitelist else - SEARCH_PATH=. + SEARCH_ARRAY=(.) fi # Recursively search for git repositories - PROJECT_DIRS=$(find ${INCLUDE_LINKS} ${SEARCH_PATH} -maxdepth ${MAXDEPTH} -mindepth 0 -name .git) + while IFS= read -r dir; do + [[ -n "$dir" ]] && PROJECT_DIRS_ARRAY+=("$dir") + done < <(find ${INCLUDE_LINKS} "${SEARCH_ARRAY[@]}" -maxdepth "${MAXDEPTH}" -mindepth 0 -name .git 2>/dev/null) elif [[ -f ".git" || -d ".git" ]]; then - PROJECT_DIRS=("$(pwd)/.git") + PROJECT_DIRS_ARRAY=("$(pwd)/.git") fi -# if project directories is still empty +# If .gsuconf provided includes, add them to the project list +if [[ -n ${GSU_INCLUDES[@]} ]]; then + # Add includes (make paths absolute relative to RAN_FROM_DIR) + for inc in "${GSU_INCLUDES[@]}"; do + [[ "$inc" = /* ]] || inc="${RAN_FROM_DIR%/}/$inc" + # strip trailing /.git if present + inc="${inc%/.git}" + inc_abs=$(abspath "$inc" 2>/dev/null || echo "") + if [[ -n "$inc_abs" && -d "$inc_abs/.git" ]]; then + PROJECT_DIRS_ARRAY+=("${inc_abs%/}/.git") + elif [[ -n "$inc_abs" && -d "$inc_abs" && -f "$inc_abs/HEAD" ]]; then + # probably a bare repository + PROJECT_DIRS_ARRAY+=("${inc_abs%/}") + fi + done +fi + +# Filter excludes from PROJECT_DIRS_ARRAY +if [[ -n ${GSU_EXCLUDES[@]} ]]; then + declare -a filtered_array + for pdir in "${PROJECT_DIRS_ARRAY[@]}"; do + if [[ "$pdir" = */.git ]]; then + proj=$(abspath "$(dirname "$pdir")") + else + proj=$(abspath "$pdir") + fi + skip=0 + for excl in "${GSU_EXCLUDES[@]}"; do + [[ "$excl" = /* ]] || excl="${RAN_FROM_DIR%/}/$excl" + excl="${excl%/.git}" + excl_abs=$(abspath "$excl" 2>/dev/null) + if [[ -z "$excl_abs" ]]; then + echo "${YELLOW}Warning: exclude path '$excl' does not exist or could not be resolved${NORMAL}" >&2 + continue + fi + if [[ "$proj" == "$excl_abs" || "$proj" == "$excl_abs/"* ]]; then + skip=1 + break + fi + done + [[ $skip -eq 0 ]] && filtered_array+=("$pdir") + done + PROJECT_DIRS_ARRAY=("${filtered_array[@]}") +fi + +# Deduplicate projects by canonical project root (always run) +declare -A final_seen=() +declare -a final_dirs=() +for pdir in "${PROJECT_DIRS_ARRAY[@]}"; do + if [[ "$pdir" = */.git ]]; then + root=$(abspath "$(dirname "$pdir")") + else + root=$(abspath "$pdir") + fi + if [[ -z "${final_seen[$root]}" ]]; then + final_seen[$root]=1 + if [[ -d "$root/.git" ]]; then + final_dirs+=("${root%/}/.git") + else + final_dirs+=("$root") + fi + fi +done + +PROJECT_DIRS_ARRAY=("${final_dirs[@]}") + +# if project directories is still empty and no .gsuconf was used # we might be sitting inside a git repo -if [[ -z ${PROJECT_DIRS} ]]; then +if [[ ${#PROJECT_DIRS_ARRAY[@]} -eq 0 && ${#GSU_INCLUDES[@]} -eq 0 && ${#GSU_EXCLUDES[@]} -eq 0 ]]; then ROOT_DIR_COMMAND="git rev-parse --show-toplevel" PROJECT_ROOT=$(eval "${ROOT_DIR_COMMAND}" 2> /dev/null) @@ -248,19 +404,22 @@ if [[ -z ${PROJECT_DIRS} ]]; then exit 0 fi - PROJECT_DIRS=("${PROJECT_ROOT}/.git") + PROJECT_DIRS_ARRAY=("${PROJECT_ROOT}/.git") fi # Foreach of the project directories, run the standup -IFS=$'\n' -for DIR in ${PROJECT_DIRS}; do - PROJECT_DIR=$(dirname "$DIR") +for DIR in "${PROJECT_DIRS_ARRAY[@]}"; do + if [[ "$DIR" = */.git ]]; then + PROJECT_DIR=$(dirname "$DIR") + else + PROJECT_DIR="$DIR" + fi cd "$PROJECT_DIR" || exit CUR_DIR=$(pwd) BASENAME=$(basename "$CUR_DIR") # continue if not a git directory - if [[ ! -d ".git" || -f ".git" ]]; then + if [[ ! -d ".git" && ! -f ".git" ]]; then cd "${BASE_DIR}" || exit continue fi diff --git a/package.json b/package.json index b4ec14e..d448c40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-standup", - "version": "2.3.2", + "version": "2.4.0", "description": "Recall what you did on the last working day. Psst! or be nosy and find what someone else in your team did ;-)", "main": "", "scripts": { @@ -23,4 +23,4 @@ "url": "https://github.com/kamranahmedse/git-standup/issues" }, "homepage": "https://github.com/kamranahmedse/git-standup#readme" -} +} \ No newline at end of file