Git-native issue tracking tool
git issue
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060#!/bin/bashWT_FOLDER=".git-pad"print_hline() { local cols cols=$(tput cols 2>/dev/null || echo 80) printf '%*s\n' "$cols" '' | tr ' ' '='}is_int() { [[ $1 =~ ^-?[0-9]+$ ]]}# Remote Protocol# file://# path starting with / or not, ending with .git# git://# https://# ssh://# [user@]server: [^/]+@[^/]+: as an SSH URL# Git's disambiguation rules# 1. If the string contains a / before the :, it is never scp-like.# 2. On Windows, C:\, D:repo are never scp-like.# 3. For host:path, Git checks:# host has no slashes# host contains only hostname-legal characters# path is not empty#is_git_scp_like_host_path() { local url="$1" local host path host_part # must contain exactly one colon (first colon matters) case "$url" in *:*) ;; *) return 1 ;; esac host_part="${url%%:*}" path="${url#*:}" # path must not be empty to be scp-like [ -n "$path" ] || return 1 # reject if '/' appears before the colon case "$host_part" in */*) return 1 ;; esac # strip optional user@ (take part after last @) host="${host_part##*@}" # host must be non-empty [ -n "$host" ] || return 1 # hostname-legal characters: [A-Za-z0-9.-]+ # Use case statement for character validation instead of regex # First check if it contains any invalid characters case "$host" in *[!A-Za-z0-9.-]*) return 1 ;; # Contains invalid chars "") return 1 ;; # Empty string esac # Windows drive-letter exclusion (only if running on Windows Git) # Safe to keep on Unix — it never triggers case "${OSTYPE:-}" in msys*|cygwin*) # Check if host is a single letter (Windows drive) case "$host" in [A-Za-z]) return 1 ;; esac ;; esac return 0 # scp-like}safe_cd() { local path="$1" if [ ! -e "$path" ]; then echo '!E: '"directory $path does not exist" >&2 return 1 elif [ ! -d "$path" ]; then echo '!E: '"$path is not a directory" >&2 return 1 elif [ ! -x "$path" ]; then echo '!E: '"Permission denied (no execute permission): $path" >&2 return 1 elif ! cd "$path" 2>&1; then echo '!E: '"Cannot cd to $path (unknown reason)" >&2 return 1 fi return 0}git_project_root() { git worktree list --porcelain | awk '$1=="worktree" { print $2; exit }' # You can use # dirname "$(git rev-parse --git-common-dir)" # Testing # if [ "$(git_project_root)" != "$(realpath $(dirname $(git rev-parse --git-common-dir)))" ]; then echo "alarm!"; fi # git rev-parse --git-common-dir return . in the root directory}git_pad_worktree() { # shellcheck disable=SC2155 local root="$(git_project_root)" issues_rootdir="$root/$WT_FOLDER" || { echo "" return 1 } echo "$issues_rootdir"}issue_ref() { echo "refs/issues/latest"}issue_audit_ref() { local local_id local_id="$(cat "$(git_pad_worktree)/.local-repo-id" 2> /dev/null || echo "local")" echo "refs/issues/audit/$local_id"}remote_issue_ref() { local remote="$1" #if git config --get remote."$remote".url > /dev/null 2>&1; then echo "refs/remotes/$remote/issues/latest" #else # echo $remote #fi}issue_git() { #root="$(git_project_root)" wt="$(git_pad_worktree)" #git -C "$root/$WT_FOLDER" "$@" git -C "$wt" "$@"}git_pad_clone() { root="$(git_project_root)" #remote=$(git_pad_remote) local wt wt=$(git_pad_worktree) cd "$root" > /dev/null || return 1 if [ $# -eq 1 ]; then remote="$1" fi # elif [ $# -lt 1 ]; then # echo '!E: Usage git pad clone <remote>' # return 1 # fi if ! remote=${remote:-$(resolve_issue_remote)}; then echo '!E: No remote, no default' echo '- Uage: git pad clone <remote>' cd - > /dev/null || return 1 return fi local local_ref remote_ref local_ref=$(issue_ref) remote_ref=$(remote_issue_ref "$remote") # git fetch "${opts[@]}" "$remote" \ # "$(issue_ref):$(remote_issue_ref "$remote")" git fetch "$remote" \ "$(issue_ref):$(remote_issue_ref "$remote")" #git worktree add $WT_FOLDER $remote/issues/latest && cd $WT_FOLDER && git update-ref "$local_ref" HEAD git worktree add "$WT_FOLDER" "$remote_ref" && cd "$WT_FOLDER" && git update-ref "$local_ref" HEAD && git update-ref "$local_ref" "$remote_ref" cd - > /dev/null || return 1 return}#! TODO: config.worktree, core.bar, core.worktreegit_pad_init() { root="$(git_project_root)" wt="$(git_pad_worktree)" [ -d "$wt" ] && { remote=${remote:-$(resolve_issue_remote)} echo "$WT_FOLDER already exists. Use the following to remove it" echo "" echo " git worktree remove $wt # To remove forcefully, use --force" echo " git update-ref -d $(issue_ref)" echo " git update-ref -d $(remote_issue_ref "$remote")" return 1 } CLONE=ask if [ $# -gt 0 ]; then for i in "$@"; do case $i in --no-clone|-n) CLONE=false ;; --clone|-c) CLONE=true ;; *) # Handle unknown options or positional arguments echo '!W: Unknown option: '"$i" ;; esac done fi #remote="$1" remote=$(resolve_issue_remote) local wt wt=$(git_pad_worktree) local local_ref remote_ref local_ref="$(issue_ref)" remote_ref="$(remote_issue_ref "$remote")" #echo "$WT_FOLDER/" >> "$root/.git/info/exclude" echo "$WT_FOLDER/" >> "$root/.gitignore" if git ls-remote --exit-code "$remote" "$local_ref" > /dev/null 2>&1; then # ref exists on the remote echo '!W: The reference '"$local_ref"' exists on the remote('"$remote"').' if [ $CLONE = "ask" ]; then while true; do read -p ' - Do you want to clone it? (Y/N) ' ans case "$ans" in [Yy]) CLONE=true break ;; [Nn]) CLONE=false break ;; esac done fi if [ $CLONE = "true" ]; then # Possibly post-commit hook for auto-sync issue-status(open->closed) echo "- cloning the issue remote $remote" git_pad_clone "$remote" git config --local extensions.worktreeConfig true || echo 'FAILED: git config --local extensions.worktreeConfig true' cd "$wt" && { git config --worktree advice.detachedHead false #git config --worktree core.worktree "$wt" git config --worktree core.hooksPath /dev/null || echo 'FAILED: git config --worktree core.hooksPath /dev/null' # mkdir -p "$(git rev-parse --git-dir)/info" # echo ".local-repo-id" >> "$(git rev-parse --git-dir)/info/exclude" #echo ".gitignore" >> .gitignore #echo ".local-repo-id" >> .gitignore } cd - > /dev/null || return 1 return fi fi #: since git worktree add --orphan is used, no more needed? # if git rev-parse --verify HEAD >/dev/null 2>&1; then # has_commit=1 # else # has_commit=0 # echo "Needs at least one commit before git pad init" # return 0 # fi # Create orphan detached worktree # git worktree add --detach "$root/$WT_FOLDER" # cd "$root/$WT_FOLDER" || return 1 # Platform-agnostic timestamp generation if command -v gdate >/dev/null 2>&1; then # macOS with GNU coreutils installed timestamp=$(gdate +%s) elif date +%s >/dev/null 2>&1; then # Linux or macOS with recent date timestamp=$(date +%s) else # Fallback for older systems timestamp=$(perl -e 'print time') fi tmp_ref="refs/issues/tmp/${timestamp}-$$" #git worktree add --orphan -B temp_issues $root/$WT_FOLDER git worktree add --orphan -B "$tmp_ref" "$root/$WT_FOLDER" mkdir -p "$root/$WT_FOLDER/issues" git config --local extensions.worktreeConfig true || echo 'FAILED: git config --local extensions.worktreeConfig true' cd "$wt" || { echo "failed to cd to issue worktree" return 1 } git config --worktree advice.detachedHead false #git config --worktree core.worktree "$wt" git config --worktree core.hooksPath /dev/null || echo 'FAILED: git config --worktree core.hooksPath /dev/null' # echo ".local-repo-id" >> "$(git rev-parse --git-dir)/info/exclude" # This can not be applied when `git pad clone` unless `git pad init` is always done before `git pad clone` cat > .gitignore << 'EOF'# Ignore everything in $WT_FOLDER/ except TEMPLATE.md .local-repo-idgithub/#TEMPLATE*.md#!TEMPLATE*.md/EOF cat > TEMPLATE.md << 'EOF'---title:type: bug|feature|task|metastatus: open|in-progress|closedpriority: P1|P2|P3severity: critical|high|medium|low---## Description## Steps to Reproduce## Expected Behavior## NotesEOF # cp TEMPLATE.md $WT_FOLDER/TEMPLATE.md # cat > issues/0001.md <<'EOF' #--- #title: Initial issue tracking setup #type: meta #priority: low #--- # #Issue tracking initialized. #EOF git add . git commit -m "issue init: templates loaded" commit=$(git rev-parse HEAD) #git update-ref "$ISSUE_REF_PREFIX/$issue_id" git update-ref refs/issues/latest "$commit" echo "Initialized issue tracking in $WT_FOLDER/" git checkout --detach #git branch -D temp_issues git branch -D "$tmp_ref" cd - > /dev/null || { echo "failed to restore the directory" return 1 }}parse_issue_id() { local id="$1" local id_left id_right if echo "$id" | grep -q '-'; then # Normalize ID format id_left="${id%-*}" id_right="${id#*-}" #echo "$id_left" "$id_right" if [ "$id_left" = "$id_right" ]; then echo 'usage: git pad edit <id>' echo ' <id> should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi if ! is_int "$id_right"; then echo 'usage: git pad edit <id>' echo ' <id> should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi else # Extract alphabetic prefix and numeric suffix without regex # Remove all digits from the end to get prefix id_left="$id" while [ "$id_left" != "${id_left%[0-9]}" ]; do id_left="${id_left%[0-9]}" done # Remove all non-digits from the beginning to get suffix id_right="$id" while [ "$id_right" != "${id_right#[!0-9]}" ]; do id_right="${id_right#[!0-9]}" done # Validate that we have both parts if [ -z "$id_left" ] || [ -z "$id_right" ]; then echo "usage: git pad edit <id>" echo ' <id> should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 fi # Verify id_left contains only alphabets and id_right contains only digits case "$id_left" in *[!A-Za-z]*) echo "usage: git pad edit <id>" echo ' <id> should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 ;; esac case "$id_right" in *[!0-9]*) echo "usage: git pad edit <id>" echo ' <id> should be alphabets-integers like PACMAN-0012, PACMAN-12, or PACMAN12' return 1 ;; esac fi #echo "$id_left" "$id_right" # Remove leading zeros - platform agnostic id_right=$(echo "$id_right" | sed 's/^0*//') # Handle empty string (if input was all zeros like "0000") [ -z "$id_right" ] && id_right="0" # Format with leading zeros (converts to decimal, avoiding octal interpretation) id_right=$(printf "%04d" "$id_right") #echo "$id_left" "$id_right" id="$id_left"-"$id_right" echo "$id"}git_pad_edit() { [ $# -eq 0 ] && { echo "usage: git pad comment <issue id>"; return 1; } #echo gie1 id="$1" id=${id%.md} id="$(parse_issue_id "$id")" || return 1 #echo gie2 fn_md="$id".md root=$(git_project_root) wt=$(git_pad_worktree) path="$wt"/issues/"$fn_md" if ! safe_cd "$wt"; then echo '!E: '"Could not change directory to $wt" return 1 fi #fn=$1 [ -z "$id" ] && { echo "usage: git pad edit <id>" cd - > /dev/null || return 1 return 1 } [ -f "$path" ] || { echo "Issue $id not found" cd - > /dev/null || return 1 return 1 } # if ! is_int "$id"; then echo 'usage: git pad show <id>'; echo ' <id> should be an integer'; return 1; # else # id=$(printf "%04d" $id) # fi tmpfile="$(mktemp)"".md" trap 'rm -f "$tmpfile"' EXIT INT TERM cp "$path" "$tmpfile" editor=$(git var GIT_EDITOR) || { rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 } #${EDITOR:-vi} "$path" sh -c "$editor \"$tmpfile\"" || { echo "editor failed" rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 } echo $tmpfile # message="$( # sed \ # -e "s/[[:space:]]*$//" \ # "$tmpfile" \ # | sed '/./,$!d' \ # | awk 'NF {p=1} p; END {while (sub(/\n$/,"")) {}}' # )" if cmp -s "$tmpfile" "$wt/TEMPLATE.md"; then echo "- Just template. Aborted." cd - > /dev/null || return 1 return 1 elif cmp -s "$tmpfile" "$path"; then echo "- Nothing changed. Aborted." cd - > /dev/null || return 1 return 1 else mv $tmpfile $path echo "- Saved to issue id $id" cd - > /dev/null || return 1 return 0 fi # if cmp -s "$path" "$root/$WT_FOLDER/TEMPLATE.md"; then # echo "No changes made to issue $path" # read -p "Do you want to keep the file? (Y/n): " ans # case "$ans" in # [Nn]*) # rm -f "$path" # echo "Deleted template issue file $path" # ;; # *) # echo "Keeping template issue file $path" # ;; # esac # else # return 0 # fi }git_pad_comment() { [ $# -eq 0 ] && { echo "usage: git pad comment <issue id> [[-m|--message]=]"; return 1; } message_next=fasle for arg in "$@"; do case "$arg" in --message=*|-m=*) MSG_CLI="${arg#*=}" ;; --message|-m) message_next=true ;; --help) echo "usage: git pad comment <issue id> [[-m|--message]=]" return 1 ;; *) if [ "$message_next" = "true" ]; then MSG_CLI="$arg" else id=${id:-} if [ -z "$id" ]; then id="$arg" else echo "usage: git pad comment <issue id> [[-m|--message]=]"; return 1; fi fi ;; esac done id=${id%.md} id="$(parse_issue_id "$1")" || return 1 fn_md="$id".md root=$(git_project_root) wt=$(git_pad_worktree) path="$wt"/issues/"$fn_md" if ! safe_cd "$wt"; then return 1 fi #fn=$1 [ -z "$id" ] && { echo "usage: git pad comment <issue id>" cd - > /dev/null || return 1 return 1 } [ -f "$path" ] || { echo "Issue $id not found" cd - > /dev/null || return 1 return 1 } if ! comment_char=$(git config core.commentChar); then comment_char="${comment_char:-#}" fi echo "- comment character set to $comment_char" tmpfile="$(mktemp)"".md" trap 'rm -f "$tmpfile"' EXIT INT TERM #sed 's/^/# /' file.txt > "$tmpfile" sed 's/^/'"$comment_char "'/' "$path" >> "$tmpfile" #echo '# --- >8 ISSUE '"$id"' ---' >> "$tmpfile" DIR_COMMENT="$wt/issues/$id/comments" echo "- comment directory : $DIR_COMMENT" comment_file=$(new_filename_comment .md "$id") || { echo '!E: New comment name could not be created' rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 } echo '- new comment file named '"$comment_file" mkdir -p "$DIR_COMMENT" find "$DIR_COMMENT" -type f -print0 2>/dev/null | while IFS= read -r -d '' other_comment_file; do { echo '' echo '# --- >8 COMMENT '"$other_comment_file"' ---' sed 's/^/'"$comment_char "'/' "$other_comment_file" echo '' } >> "$tmpfile" done echo '' >> "$tmpfile" user=$(git config user.name) email=$(git config user.email) # Platform-agnostic date formatting if date "+%Y-%m-%d %H:%M:%S" >/dev/null 2>&1; then t_now=$(date "+%Y-%m-%d %H:%M:%S") else # Fallback for systems with limited date support t_now=$(date) fi echo '*** comment by '"$user($email) at $t_now" >> "$tmpfile" MSG_CLI=${MSG_CLI:-} if [ -z "$MSG_CLI" ]; then editor=$(git var GIT_EDITOR) || { rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 } #${EDITOR:-vi} "$tmpfile" sh -c "$editor \"$tmpfile\"" || { echo '!'"W: editor failed" rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 } else #echo "" >> "$tmpfile" echo "" >> "$tmpfile" echo "$MSG_CLI" >> "$tmpfile" fi # sed '/^'"$comment_char"'/d' "$tmpfile" > "$comment_file" # if [ -s "$comment_file" ]; then # echo "- comment file saved to $comment_file" # else # echo "- no comment to save. aborted" # fi message="$( sed \ -e "s/[[:space:]]*$//" \ -e "/^""$comment_char""[[:space:]]./d" \ -e "/^""$comment_char""$/d" \ "$tmpfile" \ | sed '/./,$!d' \ | awk 'NF {p=1} p; END {while (sub(/\n$/,"")) {}}' )" echo "- message : $message" # shellcheck disable=SC2001 message_trimmed="$(echo "$message" | sed 's/[*][*][*] comment by .* at .*$//')" if [ -z "$message_trimmed" ]; then echo "Aborted: empty message" >&2 rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1 return 1 fi # Save result echo "- comment_file : $comment_file" printf "%s\n" "$message" > "$DIR_COMMENT"/"$comment_file" && echo "- comment file saved to $comment_file" # if cmp -s "$path" "$root/$WT_FOLDER/TEMPLATE.md"; then # echo "No changes made to issue $path" # printf "Do you want to keep the file? (Y/n): " # read -r ans # case "$ans" in # [Nn]*) # rm -f "$path" # echo "Deleted template issue file $path" # ;; # *) # echo "Keeping template issue file $path" # ;; # esac # else # return 0 # fi rm -f "$tmpfile" trap - EXIT INT TERM cd - > /dev/null || return 1}git_pad_new() { root=$(git_project_root) # cd "$root/$WT_FOLDER" || return 1 wt=$(git_pad_worktree) if ! safe_cd "$wt"; then echo '!E:'" Could not change directory to $wt" return 1 fi # Initialize variables with defaults TITLE="" BODY="" TYPE="" STATUS="" PRIORITY="" # Loop through all arguments for i in "$@"; do case $i in --title=*) # Use parameter expansion to strip "--title=" from the front TITLE="${i#*=}" shift # move past the argument ;; --body=*) BODY="${i#*=}" shift ;; --type=*) TYPE="${i#*=}" shift # move past the argument ;; --status=*) STATUS="${i#*=}" shift # move past the argument ;; --priority=*) PRIORITY="${i#*=}" shift # move past the argument ;; *) # Handle unknown options or positional arguments # echo "Unknown option: $i" ;; esac done fn_md=$(new_filename ".md") || { echo "failed to generate new filename" return 1 } #echo 1 root=$(git_project_root) path="$wt/issues/$fn_md" #echo 2 mkdir -p "$(dirname "$path")" #echo 3 # Validation (Optional) if [ -z "$TITLE" ]; then #echo 4 [ -f "$path" ] || cp "$wt/TEMPLATE.md" "$path" id=$(basename "$fn_md" .md) #echo 5 #echo "- id=$id" git_pad_edit "$id" || { return 1; } if [ -s "$path" ]; then echo '- issue '"$fn_md"' created' fi cd - > /dev/null || return 1 else { echo "---" echo "title: $TITLE" [ -n "$TYPE" ] && echo "type: $TYPE" [ -n "$STATUS" ] && echo "status: $STATUS" [ -n "$PRIORITY" ] && echo "priority: $PRIORITY" echo "---" echo "" echo "$BODY" } > "$path" echo "Created new issue: issues/$fn_md" cd - > /dev/null || return 1 fi}git_pad_commit() { root=$(git_project_root) local wt wt=$(git_pad_worktree) if ! safe_cd "$wt"; then return 1 fi #if git diff --cached --quiet && git diff --quiet; then # echo "No issue changes to commit" # return 0 #fi if issue_merge_in_progress; then echo "merge in progress; use git pad merge --continue" #git_pad_status; return 1 fi git add issues #if [ -n "$1" ]; then # git commit -m "$*" #else # git commit #fi # git commit "$@" # [ $? -ne 0 ] && { # echo "issue commit failed" # git restore --staged issues/ # cd - # return 1 # } if ! git commit "$@"; then echo "issue commit failed" git restore --staged issues/ cd - return 1 fi git update-ref refs/issues/latest HEAD cd - || return 1}git_pad_log() { root="$(git_project_root)" wt=$(git_pad_worktree) if ! remote_tracking=$(git config --get issue.remote); then remote_tracking=${remote_tracking:-$(resolve_issue_remote)}; fi local audit=0 for arg in "$@"; do case $arg in --audit) audit=1 shift ;; esac done if ! safe_cd $wt; then #echo 'No .issue folder' echo "Issue worktree not yet initiated($WT_FOLDER does not exist)" >&2 echo " - use 'git pad init' to create one" >&2 return 1 fi if ! git -C "$root/$WT_FOLDER" rev-parse refs/remotes/"$remote_tracking"/issues/latest >/dev/null 2>&1; then ref_remote_tracking="" # if ! git pad fetch "$remote_tracking"; then # echo '!E: git pad fetch '"$remote_tracking"' failed!' # ref_remote_tracking="" # else # ref_remote_tracking=refs/remotes/"$remote_tracking"/issues/latest # fi else ref_remote_tracking=refs/remotes/"$remote_tracking"/issues/latest fi if [[ $audit -eq 1 ]]; then #cd $wt && local_id=$(secure_repo_id) echo '> audit log' local_id="$(cat "$wt/.local-repo-id" 2> /dev/null)" # if [[ ! -f "$wt/.local-repo-id" ]]; then # echo '!E: local-repo-id not found or can'\''t be generated' # return 1 # fi echo "- local-repo-id: $local_id" echo "- args: $@" git -C "$root/$WT_FOLDER" log --oneline --graph \ --pretty=format:"%C(yellow)%h%C(reset) %C(magenta)%d%C(reset) %s" \ --decorate \ --decorate-refs=HEAD \ --decorate-refs=refs/issues/latest \ --decorate-refs=refs/remotes/"$remote_tracking"/issues/latest \ --decorate-refs=refs/issues/audit/* \ --decorate-refs-exclude=refs/heads/* \ --glob='refs/issues/*' \ $@ \ HEAD refs/issues/latest $ref_remote_tracking #\ #--decorate-refs=refs/issues/audit/"$local_id" else git -C "$root/$WT_FOLDER" log --oneline --graph \ --decorate-refs=HEAD \ --decorate-refs=refs/issues/latest \ --decorate-refs=refs/remotes/"$remote_tracking"/issues/latest \ --decorate-refs-exclude=refs/heads/* \ $@ \ HEAD refs/issues/latest $ref_remote_tracking #\ #--decorate-refs=refs/issues/audit/"$local_id" fi cd - >/dev/null || return 1}# git_pad_list() {# root=$(git_project_root)# [ ! -d "$root/$WT_FOLDER" ] && echo ".issue not found. Use \`git pad init\` to make one" && return 1# ls "$root/$WT_FOLDER/issues" | sed 's/\.md$//'# }count_comments() { # Count comment files id="$1" root=$(git_project_root) issues_dir="$root/$WT_FOLDER/issues" || return 1 comments_dir="$issues_dir/$id/comments" if [ -d "$comments_dir" ]; then num_comments=$(find "$comments_dir" -type f | wc -l) else num_comments=0 fi echo "$num_comments"}git_pad_show() { if [ $# -lt 1 ]; then echo '!E: Usage git pad show <id>' return 1 fi PRINT_COMMENTS=ask for arg in "$@"; do case "$arg" in --help) echo '- Usage: git pad show [--comments|--no-comments] <id>' return 1 ;; --comments) PRINT_COMMENTS=true ;; --no-comments) PRINT_COMMENTS=false ;; *) id="$arg" ;; esac done id=${id%.md} id="$(parse_issue_id "$id")" || return 1 [ -z "$id" ] && echo "usage: git pad show <id>" && return 1 #if ! is_int "$id"; then echo 'usage: git pad show <id>'; echo ' <id> should be an integer'; return 1; #else #id=$(printf "%04d" $id) root=$(git_project_root) wt=$(git_pad_worktree) file="$root/$WT_FOLDER/issues/$id.md" [ -f "$file" ] || { echo "Issue $id not found" return 1 } echo '' print_hline echo "ISSUE-FILE: issues/$id.md" sed -n '1,200p' "$file" #fi num_comments=$(count_comments "$id") print_hline echo "- Comments found : $num_comments" echo "" if [ "$num_comments" -gt 0 ]; then if [ $PRINT_COMMENTS = "ask" ]; then while :; do printf "%s" "--->8 Continue to comments? (Y/N) " read -r ans case "$ans" in [Nn]) PRINT_COMMENTS=false break ;; [Yy]) PRINT_COMMENTS=true break ;; *) # Invalid input, loop continues ;; esac done fi if [ $PRINT_COMMENTS = "false" ]; then return 0 fi DIR_COMMENT="$wt/issues/$id/comments" if [ -d "$DIR_COMMENT" ]; then # Platform-agnostic approach using a temporary file for sorting tmp_sorted=$(mktemp) trap 'rm -f "$tmp_sorted"' EXIT INT TERM # Get files with modification times find "$DIR_COMMENT" -type f -print0 2>/dev/null | while IFS= read -r -d '' f; do # Get modification time in a platform-agnostic way if stat -f "%m" "$f" >/dev/null 2>&1; then # macOS (BSD stat) mtime=$(stat -f "%m" "$f") else # Linux (GNU stat) mtime=$(stat -c "%Y" "$f") fi printf "%s %s\n" "$mtime" "$f" done | sort -n | cut -d' ' -f2- > "$tmp_sorted" # Process each file (oldest to newest) while IFS= read -r other_comment_file; do [ -f "$other_comment_file" ] || continue echo '' echo '=== >8 COMMENT '"$other_comment_file"' ---' #cat "$other_comment_file" sed 's/^/ /' "$other_comment_file" echo '' done < "$tmp_sorted" rm -f "$tmp_sorted" trap - EXIT INT TERM fi fi}git_pad_list() { # 1. 모든 변수를 local로 선언하고 초기화 (unbound variable 방지) local root="" root=$(git_project_root) local issues_dir="$root/$WT_FOLDER/issues" [ ! -d "$issues_dir" ] && echo ".issue not found..." && return 1 local filter_status="" local filter_type="" local sort_mode="" local US=$'\x1f' # --- 인자 파싱 --- while [[ "$#" -gt 0 ]]; do case $1 in --filter-status=*) filter_status="${1#*=}"; shift ;; --filter-type=*) filter_type="${1#*=}"; shift ;; --sort-comments|--sort-comment|--sort-cmt) [[ -n "$sort_mode" ]] && echo "Error: Sort mode already set" && return 1 sort_mode="comments"; shift ;; --sort-priorities|--sort-priority|--sort-pri) [[ -n "$sort_mode" ]] && echo "Error: Sort mode already set" && return 1 sort_mode="priority"; shift ;; --open) filter_status="open"; shift ;; --closed) filter_status="closed"; shift ;; *) shift ;; esac done [[ -z "$sort_mode" ]] && sort_mode="mtime" # 변수 초기 선언 시 빈 값이라도 넣어줘야 set -u에서 안전합니다. local data_file="" data_file=$(mktemp) # 2. 데이터 수집 루프 (루프 내 변수들도 local 처리) # find 결과가 없을 경우를 대비해 || true 등을 고려할 수 있음 while IFS= read -r -d '' f; do local mtime="" local id="" local num_comments=0 if stat -f "%m" "$f" >/dev/null 2>&1; then mtime=$(stat -f "%m" "$f") else mtime=$(stat -c "%Y" "$f") fi id=$(basename "$f" .md) num_comments=$(count_comments "$id") if [ "$num_comments" -eq 0 ]; then num_comments=" " fi awk -v mtime="$mtime" -v id="$id" -v n_cmt="$num_comments" \ -v target_status="$filter_status" -v target_type="$filter_type" ' BEGIN { in_yaml = 0; title=type=pri=status="-" } /^---$/ { in_yaml = !in_yaml; next } in_yaml { if ($1 == "title:") { sub(/^title:[[:space:]]*/, ""); title = $0 } else if ($1 == "type:") { sub(/^type:[[:space:]]*/, ""); type = $0; gsub(/[[:space:](].*$/, "", type) } else if ($1 == "priority:") { pri = $2 } else if ($1 == "status:") { sub(/^status:[[:space:]]*/, ""); status = $0; gsub(/[[:space:](].*$/, "", status) } } END { match_status = (target_status == "" || status == target_status) match_type = (target_type == "" || type == target_type) if (match_status && match_type) { printf "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n", mtime, id, status, type, pri, n_cmt, title } } ' "$f" done < <(find "$issues_dir" -maxdepth 1 -name "*.md" -type f -print0) > "$data_file" # 3. 정렬 및 출력 local sorted_data="" case $sort_mode in comments) sorted_data=$(sort -t"$US" -k6,6nr "$data_file") ;; priority) sorted_data=$(sort -t"$US" -k5,5 "$data_file") ;; *) sorted_data=$(sort -t"$US" -k1,1nr "$data_file") ;; esac printf "%-13s %-8s %-8s %-3s %-4s %s\n" "ID" "STATUS" "TYPE" "PRI" "#CMT" "TITLE" # 정렬 데이터가 비어있을 수 있으므로 체크 if [[ -n "$sorted_data" ]]; then while IFS="$US" read -r mt id st ty pr cm ti; do [[ -z "$id" ]] && continue printf "%-13s %-8s %-8s %-3s %4s %s\n" \ "$id" "${st:0:8}" "${ty:0:8}" "${pr:0:3}" "$cm" "$ti" done <<< "$sorted_data" fi # 4. 수동 정리 (함수가 끝나기 전에 임시 파일 삭제) rm -f "$data_file"}upper() { printf "%s" "$1" | tr '[:lower:]' '[:upper:]'}truncate() { max=$1 shift s="$*" [ "${#s}" -le "$max" ] && printf "%s" "$s" || printf "%s…" "${s:0:max}"}rel_time() { delta=$1 # seconds if [ "$delta" -lt 3600 ]; then printf -- "< %2dM" $((delta / 60 + 1)) elif [ "$delta" -lt 86400 ]; then printf -- "< %2dH" $((delta / 3600 + 1)) elif [ "$delta" -lt 2592000 ]; then printf -- "< %2dd " $((delta / 86400 + 1)) elif [ "$delta" -lt 31536000 ]; then printf -- "< %2dm " $((delta / 2592000 + 1)) else printf -- "< %2dy " $((delta / 31536000 + 1)) fi}priority_char() { case "$1" in "P1") PRI_MAP="1 " ;; "P2") PRI_MAP=" 2 " ;; "P3") PRI_MAP=" 3" ;; *) PRI_MAP=" ?" ;; # Default for unknown values esac echo "$PRI_MAP"}git_pad_list_full() { root=$(git_project_root) issues_rootdir="$root/$WT_FOLDER" || return 1 issues_dir="$root/$WT_FOLDER/issues" || return 1 wt=$(git_pad_worktree) if ! safe_cd "$wt"; then return 1 fi # This is safter and the following cd # safe_cd "$issues_rootdir" || return 1 # TYPE PRI TITLE CHANGED printf "%-11s %-2s %-4s %-3s %-40s %-7s %-35s %s\n" \ "ID" "ST" "TYPE" "PRI" "TITLE" "CHANGED" "COMMIT" "CMT_TIME" for f in "$issues_dir"/*.md; do [ -f "$f" ] || continue id=$(basename "$f" .md) rel="issues/$id.md" # -------- frontmatter -------- # Read into variables without eval to avoid command injection while IFS= read -r line; do case "$line" in status:*) status="${line#status:}" status="${status#"${status%%[![:space:]]*}"}" ;; type:*) type="${line#type:}" type="${type#"${type%%[![:space:]]*}"}" ;; priority:*) pri="${line#priority:}" pri="${pri#"${pri%%[![:space:]]*}"}" ;; title:*) title="${line#title:}" title="${title#"${title%%[![:space:]]*}"}" ;; esac done < <(awk ' BEGIN { in_yaml = 0 } /^---$/ { in_yaml = !in_yaml; next } in_yaml && /^(status|type|priority|title):/ { print } ' "$f") ST=$(upper "${status:-??}") ST=${ST:0:2} TYPE=$(upper "${type:-????}") TYPE=${TYPE:0:4} PRI=$(upper "${pri:-???}") PRI=${PRI:0:3} TITLE=$(truncate 39 "${title:-}") PRI_MAP=$(priority_char "$PRI") # -------- dirty / changed -------- if git status --porcelain -- "$rel" | grep -q .; then now=$(date +%s) mtime=$(stat -c %Y "$f" 2> /dev/null || stat -f %m "$f") delta=$((now - mtime)) CHANGED=$(rel_time "$delta") else CHANGED="" fi # -------- last commit -------- read -r cdate ctime cmsg << EOF$(git log -1 --pretty=format:'%cd %s' --date=format:'%Y-%m-%d %H:%M' -- "$rel" 2> /dev/null)EOF COMMIT=$(truncate 34 "$cmsg") CMT_TIME=${cdate%:*} printf "%-11s %-2s %-4s %-3s %-40s %-7s %-35s %s\n" \ "$id" "$ST" "$TYPE" "$PRI_MAP" "$TITLE" "$CHANGED" "$COMMIT" "$CMT_TIME" done | sort echo '===============================' echo '* '"CHANGED: relative time since last modification(uncommitted changes)" cd - > /dev/null || return 1}#git_pad_list_fullgit_pad_search() { # for f in $(git pad search --title='[Tt]est'); do echo '========================='; echo '= '$f; echo ""; grep -C 5 -i Test $f; done local pat= local title_pat= local body_pat= local ignore_case=0 root=$(git_project_root) issues_rootdir="$root/$WT_FOLDER" || return 1 issues_dir="$root/$WT_FOLDER/issues" || return 1 # parse args for arg in "$@"; do case $arg in --title=*) title_pat=${arg#*=} ;; --body=*) body_pat=${arg#*=} ;; -i) ignore_case=1 ;; *) pat=$arg ;; esac done #echo "$title_pat" #echo "$body_pat" #echo "$ignore_case" awk -v pat="$pat" \ -v title_pat="$title_pat" \ -v body_pat="$body_pat" \ -v icase="$ignore_case" ' function finish_file() { if (prev_filename == "") return match_file = 1 # Case-insensitive preparation content_to_check = (icase ? tolower(body) : body) t_pat = (icase ? tolower(title_pat) : title_pat) b_pat = (icase ? tolower(body_pat) : body_pat) g_pat = (icase ? tolower(pat) : pat) # 1. Title Specific Filter if (title_pat != "") { found_title = 0 for (k in yaml) { key_match = (icase ? tolower(k) : k) if (key_match == "title") { val_match = (icase ? tolower(yaml[k]) : yaml[k]) if (val_match ~ t_pat) found_title = 1 } } if (!found_title) match_file = 0 } # 2. Body Specific Filter if (body_pat != "" && content_to_check !~ b_pat) match_file = 0 # 3. Global pattern (check keys, values, AND body) if (pat != "") { dump = body for (k in yaml) dump = dump " " k " " yaml[k] if (icase) dump = tolower(dump) if (dump !~ g_pat) match_file = 0 } if (match_file) print prev_filename } # Reset state and process previous file before starting new file FNR == 1 { if (NR > 1) finish_file() in_yaml = 0 body = "" # Portable way to clear an array split("", yaml) prev_filename = FILENAME } # YAML parsing $0 == "---" { if (FNR == 1) { in_yaml = 1; next } if (in_yaml) { in_yaml = 0; next } } in_yaml { if (match($0, /^[a-zA-Z0-9_-]+:/)) { sep = index($0, ":") key = substr($0, 1, sep - 1) val = substr($0, sep + 1) # Trim both leading and trailing whitespace gsub(/^[[:space:]]+|[[:space:]]+$/, "", val) yaml[key] = val } next } # Everything else is body { body = body $0 "\n" } END { finish_file() }' "$issues_dir"/*.md}#git_pad_search Hiresolve_issue_remote() { # 1. explicit CLI remote already handled by caller # 2. issue.remote local r if r=$(git config --get issue.remote); then echo "$r" return fi # 3. remote.pushDefault if r=$(git config --get remote.pushDefault); then echo "$r" return fi # 4. branch upstream (only if on branch) local branch root=$(git_project_root); cd "$root" > /dev/null 2>&1 || return 1 if branch=$(git symbolic-ref --quiet --short HEAD 2> /dev/null); then if r=$(git config --get branch."$branch".remote); then echo "$r" return fi fi cd - > /dev/null 2>&1 || return 1 # 5. fallback echo origin}# git_pad_remote() {# git config issue.remote 2>/dev/null ||# git config --get remote.pushDefault ||# echo origin# }git_pad_track() { #[ $# -eq 1 ] || { echo "usage: git pad track <remote>"; return 1; } if [ "$#" -gt 1 ]; then echo "usage: git pad track (<remote>|--unset)" return 1 fi if [ "$#" -lt 1 ]; then #echo "usage: git pad track <remote>" # from function resolve_issue_remote local r if r=$(git config --get issue.remote); then echo "Currently tracking $r" return fi echo "Tracking remote is not set." # 3. remote.pushDefault if r=$(git config --get remote.pushDefault); then echo "- remote.pushDefault set to $r" return fi # 4. branch upstream (only if on branch) local branch if branch=$(git symbolic-ref --quiet --short HEAD 2> /dev/null); then if r=$(git config --get branch.$branch.remote); then echo "- branch.CURRENT_BRANCH($branch).remote set to $r" return fi fi # 5. fallback echo "- remote would be origin if not specified" return 1 fi if [ "$#" -eq 1 ]; then local remote=$1 if [ "$remote" = "--unset" ]; then git config --unset issue.remote || { echo "failed to unset issue.remote" return 1 } fi # check remote exists if ! git ls-remote --exit-code "$remote" > /dev/null 2>&1; then echo "remote '$remote' not found" return 1 fi git config issue.remote "$remote" || { echo "failed to set issue.remote" return 1 } echo "Issues now track remote '$remote'" fi}# Original# git_pad_push() {# local remote=# local opts=()# remote=$(git config --get remote.pushDefault || echo origin)# for arg in "$@"; do# case "$arg" in# --force|-f)# echo "force-push is forbidden for issues"# return 1# ;;# -*)# opts+=("$arg")# ;;# *)# remote="$arg"# ;;# esac# done# git push "${opts[@]}" "$remote" \# refs/issues/latest:refs/issues/latest# }git_pad_push() { local set_upstream=0 local force=0 local remote= while [ $# -gt 0 ]; do case "$1" in -u) set_upstream=1 ;; #--force|-f) force=1 ;; *) remote=$1 ;; esac shift done if [ $set_upstream -eq 1 ] && [ -z "$remote" ]; then echo "remote must be specified when using -u" return 1 fi remote=${remote:-$(resolve_issue_remote)} url_remote="$(git remote get-url --push "$remote")" if [[ ! ("$url_remote" =~ ^file:// || "$url_remote" =~ ^ssh:// || "$url_remote" =~ ^git:// || "$url_remote" =~ ^https:// || "$url_remote" =~ ^http://) ]]; then if ! is_git_scp_like_host_path "$url_remote"; then # not scp-like if [[ ! "$url_remote" = /* ]]; then # not absolute path local root root="$(git_project_root)" echo '!W: Relative local paths would not work for worktree.' echo ' If it failes, try delete and create '"$remote"' again using the following:' echo ' - git remote remove '"$remote" echo ' - git remote add '"$remote"' '"$(realpath "$root"/"$url_remote")" echo ' - git pad push '"$remote" else echo '!W: Your remote may be ambiguous whether it is scp or local path' echo ' If it failes, try ssh:// or file://'"$url_remote" fi fi fi echo 'Pushing issues to remote '"$remote"'...' local L R L=$(issue_ref) R=$(remote_issue_ref "$remote") # ! replace die # replace die "([^"]+)" # with { echo "$1"; return 1; } # ensure local ref exists git show-ref --verify --quiet "$L" || { echo "no local issue snapshot ($L)" return 1 } # fetch remote state for safety check # git_pad_fetch "$remote" #commnted: isolate fetch from push if [ $force -eq 0 ] && git show-ref --verify --quiet "$R"; then git merge-base --is-ancestor "$R" "$L" || { echo "remote issues have advanced; do "git pad fetch" first" return 1 } fi if git push "$remote" "$L:$L"; then echo "- Push to $remote successful" if git_pad_fetch "$remote"; then echo "- Local snapshot updated" else echo "- Failed to update local snapshot" fi else echo "- Push failed" return 1 fi if [ $set_upstream -eq 1 ]; then git config issue.remote "$remote" echo "Tracking remote set to '$remote'" fi}git_pad_fetch() { #set -x local remote= local opts=() for arg in "$@"; do case "$arg" in -*) opts+=("$arg") ;; *) remote="$arg" ;; esac done if ! remote=${remote:-$(resolve_issue_remote)}; then echo '!E: No remote and no default' echo '- Usage: git pad fetch <remote>' return 1 fi local local_ref remote_ref local_ref=$(issue_ref) remote_ref=$(remote_issue_ref "$remote") # issue_git show-ref --verify --quiet "$local_ref" \ # || { echo "no local issue snapshot"; return 1; } # issue_git show-ref --verify --quiet "$remote_ref" \ # || { echo "no remote issue snapshot for '$remote'"; return 1; } REMOTE_SHA=$(git ls-remote "$remote" "$local_ref" | cut -f1) if [ -z "$REMOTE_SHA" ]; then echo "remote issue snapshot not found on '$remote'" return 1 fi if LOCAL_SHA=$(git rev-parse "$remote_ref" 2> /dev/null); then #echo "$REMOTE_SHA" if [ "$LOCAL_SHA" = "$REMOTE_SHA" ]; then echo "Nothing to fetch: local remote-tracking issue snapshot is up-to-date with '$remote'" return 0 fi fi # an older version of Bash on macOS. macOS ships with Bash 3.2 by default, which has a bug where "${opts[@]}" fails with set -u even when the array is initialized as empty. # git fetch ${opts[@]+"${opts[@]}"} "$remote" \ # "$(issue_ref):$(remote_issue_ref "$remote")" || # { # echo "fetch failed" # return 1 # } if [ ${#opts[@]} -gt 0 ]; then git fetch "${opts[@]}" "$remote" \ "$(issue_ref):$(remote_issue_ref "$remote")" || { echo "fetch failed" return 1 } else git fetch "$remote" \ "$(issue_ref):$(remote_issue_ref "$remote")" || { echo "fetch failed" return 1 } fi}# clone-issue workflow:# 1. git pad fetch# 2. git worktree add $WT_FOLDER/ origin/issues/latest# 3. git pad listissue_merge_in_progress() { issue_git rev-parse -q --verify MERGE_HEAD > /dev/null 2>&1}git_pad_merge() { local mode= local rebase=0 local remote= local opts=() local audit=0 for arg in "$@"; do case "$arg" in --help) echo '- Usage: git pad merge [--rebase] [--audit]' echo ' - Start merging remote and current HEAD.' echo ' --rebase: use rebase if possible' echo ' --audit: leave a tag for audit' echo '' echo '- Usage: git pad merge [--continue|--abort]' echo ' - Continue or abort merge(or rebase) while in conflict' echo ' --continue: continue merging after resolving conflicts manually' echo ' --abort: abort the merging after conflicts' ;; --abort) if [ "$mode" = "--continue" ]; then echo "cannot use --abort and --continue together" return 1 fi mode="--abort" ;; --continue) mode="--continue" if [ "$mode" = "--abort" ]; then echo "cannot use --abort and --continue together" return 1 fi mode="--continue" ;; --rebase) rebase=1 ;; --audit) audit=1 ;; -*) opts+=("$arg") ;; *) remote="$arg" ;; esac done #remote=$(git_pad_remote) remote=${remote:-$(resolve_issue_remote)} local wt wt=$(git_pad_worktree) local local_ref remote_ref local_ref=$(issue_ref) remote_ref=$(remote_issue_ref "$remote") issue_git show-ref --verify --quiet "$local_ref" || { echo "no local issue snapshot" return 1 } issue_git show-ref --verify --quiet "$remote_ref" || { echo "no remote issue snapshot for '$remote'" return 1 } ( cd "$wt" || { echo "failed to enter issue worktree $wt" return 1 } case $mode in --abort) #git rebase --abort 2>/dev/null || git merge --abort git merge --abort return 0 ;; --continue) if [ -f "$(git rev-parse --git-dir)"/MERGE_HEAD ]; then git add issues git commit || { echo "commit failed" return 1 } git update-ref "$local_ref" HEAD return 0 else echo "no merge to continue" return 1 fi ;; esac #git fetch "$remote" \ # "refs/issues/latest:$remote_ref" || die "fetch failed" local local_oid remote_oid local_oid=$(git rev-parse "$local_ref") remote_oid=$(git rev-parse "$remote_ref") # Fast-forward? if git merge-base --is-ancestor "$local_oid" "$remote_oid"; then # git reset --hard "$remote_oid" new_opts=() local ff=1 for opt in ${opts[@]+"${opts[@]}"}; do #if [[ "$opt" != "--ff" && "$opt" != "--no-ff" ]]; then # new_opts+=("$opt") if [[ "$opt" = "--ff" ]]; then ff=1 elif [[ "$opt" = "--no-ff" || "$opt" = "ff-only" ]]; then ff=0 new_opts+=("$opt") fi done opts=("${new_opts[@]+"${new_opts[@]}"}") if [[ $ff -eq 1 ]]; then git merge --ff ${opts[@]+"${opts[@]}"} "$remote_oid" else git merge ${opts[@]+"${opts[@]}"} "$remote_oid" fi git update-ref "$local_ref" HEAD return 0 fi #output=$(git rebase "$remote_oid" 2>&1) #if [ $? -eq 0 ]; then if [[ $audit -eq 1 ]]; then # to preserve the original commits ref_audit="$(issue_audit_ref)"/pre-rebase-"$(date +%y%m%d%H%M)" git update-ref "$ref_audit" HEAD fi if [[ $rebase -eq 1 ]]; then if output=$(git rebase "$remote_oid" 2>&1); then echo "$output" git update-ref "$local_ref" HEAD return 0 else echo "rebase failed, falling back to merge..." git update-ref -d "$ref_audit" git rebase --abort > /dev/null 2>&1 if git merge ${opts[@]+"${opts[@]}"} "$remote_oid"; then git update-ref "$local_ref" HEAD return 0 fi fi else if output=$(git merge "$remote_oid" 2>&1); then echo "$output" git update-ref "$local_ref" HEAD return 0 elif ! git rev-parse HEAD~; then if [ -n "$(git status --porcelain)" ]; then echo '!E: '"There are still uncommitted contents in $wt. Aborted" git merge --abort return 0 else echo '!W: '"You are at the root. Overwriting history" git merge -X theirs --allow-unrelated-histories "$remote_ref" git update-ref "$local_ref" HEAD return 0 fi fi fi # # Try rebase # if git rebase "$remote_oid"; then # git update-ref "$local_ref" HEAD # return 0 # fi # # Rebase failed → fallback to merge # git rebase --abort # if git merge "${opts[@]}" "$remote_oid"; then # git update-ref "$local_ref" HEAD # return 0 # fi echo "git-pad> Conflict detected. Resolve and run:" echo " git pad merge --continue" echo " git pad merge --abort" return 1 )}git_pad_status() { local wt wt=$(git_pad_worktree) ( #cd "$wt" 2> /dev/null || { echo 'E: $WT_FOLDER not yet initiated.'; echo ' - use "git pad init" and "git pad pull"'; return 1; } if ! safe_cd "$wt"; then return 1 fi issue_gitdir=$(git rev-parse --git-dir) if [ -f "$issue_gitdir"/MERGE_HEAD ]; then echo "Issue merge in progress" git status --short exit 0 fi #if git diff-index --quiet HEAD -- || \ # [ -n "$(git ls-files --others --exclude-standard)" ]; then # echo "Repository has changes or untracked files" #fi local remote_tracking #remote=$(git_pad_remote) if ! remote_tracking=$(git config --get issue.remote); then remote_tracking=${remote_tracking:-$(resolve_issue_remote)}; fi if [ -n "$remote_tracking" ]; then #echo "$remote_tracking" local local_oid remote_oid local_oid=$(git rev-parse "$(issue_ref)") #remote_oid=$(git rev-parse "$(remote_issue_ref "$remote_tracking")" 2>/dev/null) if ! remote_oid=$(git rev-parse "$(remote_issue_ref "$remote_tracking")" 2> /dev/null); then echo "No remote issue snapshot for '$remote_tracking'" #return 1 elif [ -n "$remote_oid" ]; then if [ "$local_oid" = "$remote_oid" ]; then echo "Issues up to date with tracking remote $remote_tracking" elif git merge-base --is-ancestor "$local_oid" "$remote_oid"; then printf "Issues behind tracking remote $remote_tracking by %s commits\n" \ "$(git rev-list --count "$local_oid..$remote_oid")" elif git merge-base --is-ancestor "$remote_oid" "$local_oid"; then printf "Issues ahead of tracking remote $remote_tracking by %s commits\n" \ "$(git rev-list --count "$remote_oid..$local_oid")" else printf "Issues diverged: Ahead %s, Behind %s\n" \ "$(git rev-list --count "$remote_oid..$local_oid")" \ "$(git rev-list --count "$local_oid..$remote_oid")" fi fi else echo "No tracking remote set for issues" fi # if git diff-index --quiet HEAD -- || \ # [ -n "$(git ls-files --others --exclude-standard)" ]; then # echo "No changes since last commit" # else git status --short -- issues/ # fi )}git_pad_remove() { cwd=$(pwd) wt="$(git_pad_worktree)" root="$(git_project_root)" if ! safe_cd "$wt"; then echo '!E: '"issue folder $wt does not exit" return 1 fi FORCE=ask for arg in "$@"; do case $arg in --force|-f) FORCE=true ;; --no-force|-n) FORCE=false ;; esac done st=$(git status --porcelain) if [ -n "$st" ]; then echo '!W: '"There are still uncommitted contents in $wt" if [ $FORCE = "ask" ]; then while true; do read -p ' - Do you want to remove anyway? (Y/N) ' ans case "$ans" in [Yy]) FORCE=true break ;; [Nn]) FORCE=false break ;; esac done if [ $FORCE = "false" ]; then echo ' - Aborted.' return 1 fi fi fi remote=${remote:-$(resolve_issue_remote)} if ! safe_cd "$root"; then echo '!E :'"Can't change directoroy to git root" return 1 fi if [ $FORCE = "true" ]; then #issue_git worktree remove --force "$wt" git worktree remove --force "$wt" else git worktree remove "$wt" fi git update-ref -d "$(issue_ref)" git update-ref -d "$(remote_issue_ref "$remote")" #! git worktree나 git update-ref는 꼭 $wt에서 하지 않아도 된다! echo '!I: '"worktree $wt DELETED" echo " - ref $(issue_ref) DELETED" echo " - ref $(remote_issue_ref "$remote") DELETED" echo '' echo '!I: '"To delete remote issue ref" echo ' - git push origin --delete refs/issues/latest or git push origin :refs/issues/latest' if ! safe_cd "$cwd"; then echo '!E :'"Can't change directoroy back to original directory" return 1 fi}git_pad_version() { # Copy-based install: a VERSION file is placed alongside the scripts if [[ -f "${SCRIPT_DIR}/VERSION" ]]; then cat "${SCRIPT_DIR}/VERSION" return 0 fi # PATH/source install: SCRIPT_DIR is the git-pad source repo itself if git -C "${SCRIPT_DIR}" rev-parse --git-dir >/dev/null 2>&1; then git -C "${SCRIPT_DIR}" log -1 --format="%H" return 0 fi echo "unknown"}