@@ -35,6 +35,102 @@ ok() { echo " [ok] $*" >&2; }
3535warn () { echo " [!] $* " >&2 ; }
3636fail () { echo " [✗] $* " >&2 ; }
3737
38+ # Mask credentials in URLs (https://TOKEN@github.com/... -> https://****@github.com/...)
39+ mask_url () {
40+ echo " $1 " | sed ' s|https://[^@]*@|https://****@|'
41+ }
42+
43+ # Detect personal SSH host and GitHub username.
44+ # Parses ~/.ssh/config for GitHub hosts, runs ssh -T on each,
45+ # returns the one that ISN'T the work account.
46+ # Sets: PERSONAL_SSH_HOST, PERSONAL_GH_USER
47+ detect_personal_ssh () {
48+ local work_user=" ${1:- } " work_host=" ${2:- } "
49+ PERSONAL_SSH_HOST=" "
50+ PERSONAL_GH_USER=" "
51+
52+ [[ ! -f " $HOME /.ssh/config" ]] && return 1
53+
54+ # Extract all GitHub host aliases from SSH config
55+ local hosts=()
56+ while IFS= read -r line; do
57+ # Match "Host github*" or "Host *github*" lines, extract the alias
58+ local h
59+ h=" $( echo " $line " | sed -n ' s/^[Hh]ost[[:space:]][[:space:]]*//p' | tr -d ' ' ) "
60+ [[ -n " $h " && " $h " != " *" ]] && hosts+=(" $h " )
61+ done < <( grep -i " ^[[:space:]]*host[[:space:]].*github" " $HOME /.ssh/config" 2> /dev/null | grep -iv " hostname" )
62+
63+ [[ ${# hosts[@]} -lt 2 ]] && return 1
64+
65+ for h in " ${hosts[@]} " ; do
66+ # Skip the known work host
67+ [[ " $h " == " $work_host " ]] && continue
68+
69+ # Test SSH and extract username
70+ local ssh_output
71+ ssh_output=" $( ssh -T -o BatchMode=yes -o ConnectTimeout=5 " git@$h " 2>&1 || true) "
72+ local gh_user
73+ gh_user=" $( echo " $ssh_output " | sed -n ' s/.*Hi \([^!]*\)!.*/\1/p' | head -1) "
74+
75+ if [[ -n " $gh_user " && " $gh_user " != " $work_user " ]]; then
76+ PERSONAL_SSH_HOST=" $h "
77+ PERSONAL_GH_USER=" $gh_user "
78+ return 0
79+ fi
80+ done
81+ return 1
82+ }
83+
84+ # Prompt for HTTPS token and embed in mirror remote URL.
85+ # Usage: fix_https_auth <mirror_dir> <current_url>
86+ fix_https_auth () {
87+ local mdir=" $1 " url=" $2 " mirror_token=" "
88+ info " HTTPS repos need authentication to push."
89+ echo " "
90+ info " Create a PAT on your PERSONAL GitHub account (not work):"
91+ info " https://github.com/settings/tokens/new"
92+ info " Scope: [x] repo"
93+ echo " "
94+ if confirm " Do you have a personal GitHub token for push access?" ; then
95+ printf " Paste token (hidden): " >&2
96+ read -rs mirror_token
97+ echo " " >&2
98+ else
99+ echo " "
100+ info " How to create one:"
101+ info " 1. Go to: https://github.com/settings/tokens/new"
102+ info " (Make sure you're logged into your PERSONAL GitHub, not work)"
103+ info " 2. Note: greens-push"
104+ info " 3. Expiration: 90 days"
105+ info " 4. Select scopes: [x] repo"
106+ info " 5. Click 'Generate token' and copy it"
107+ echo " "
108+ if confirm " Ready to paste the token?" ; then
109+ printf " Paste token (hidden): " >&2
110+ read -rs mirror_token
111+ echo " " >&2
112+ else
113+ mirror_token=" "
114+ fi
115+ fi
116+ if [[ -n " ${mirror_token:- } " ]]; then
117+ local authed_url
118+ authed_url=" $( echo " $url " | sed " s|https://[^@]*@|https://|; s|https://|https://${mirror_token} @|" ) "
119+ git -C " $mdir " remote set-url origin " $authed_url "
120+ if git -C " $mdir " push origin HEAD 2> /dev/null; then
121+ ok " Push access works now"
122+ return 0
123+ else
124+ warn " Still can't push. Check that the token has repo scope and the repo exists."
125+ return 1
126+ fi
127+ else
128+ warn " Skipping — commits won't push to GitHub until auth is configured."
129+ info " Fix later: greens --setup"
130+ return 1
131+ fi
132+ }
133+
38134# ─────────────────────────────────────────────────────────────────────────────
39135# Prerequisites check
40136# ─────────────────────────────────────────────────────────────────────────────
@@ -596,13 +692,53 @@ info "It can be public or private. If private, enable 'Private contributions'"
596692info " in GitHub Settings > Profile so the green squares show on your profile."
597693echo " "
598694if [[ ! -d " $mirror_dir /.git" ]]; then
599- # Offer to create via gh if available
600- if command -v gh & > /dev/null && gh auth status & > /dev/null 2>&1 ; then
695+ mirror_repo_name=" $( prompt " Repo name" " work-contributions-mirror" ) "
696+
697+ # Try to auto-detect personal SSH identity for the mirror
698+ personal_host=" "
699+ personal_user=" "
700+ if [[ " $auth_method " == " ssh" ]]; then
701+ info " Detecting your personal GitHub account from SSH config..."
702+ if detect_personal_ssh " ${github_username:- } " " ${detected_ssh_host:- } " ; then
703+ personal_host=" $PERSONAL_SSH_HOST "
704+ personal_user=" $PERSONAL_GH_USER "
705+ ok " Found personal account: $personal_user (SSH host: $personal_host )"
706+ fi
707+ fi
708+
709+ if [[ -n " $personal_user " && -n " $personal_host " ]]; then
710+ # SSH multi-account: auto-construct mirror URL using personal identity
711+ mirror_url=" git@${personal_host} :${personal_user} /${mirror_repo_name} .git"
712+ info " Mirror URL: $mirror_url "
713+ echo " "
714+
715+ # Try to create the repo on GitHub
716+ repo_created=0
717+ if command -v gh & > /dev/null && gh auth status & > /dev/null 2>&1 ; then
718+ gh_user=" $( gh api user -q .login 2> /dev/null || echo " " ) "
719+ if [[ " $gh_user " == " $personal_user " ]]; then
720+ # gh is on the personal account, create directly
721+ if gh repo create " $mirror_repo_name " --public --description " Mirror of private work contributions" 2> /dev/null; then
722+ ok " Created github.com/$personal_user /$mirror_repo_name "
723+ repo_created=1
724+ fi
725+ fi
726+ fi
727+
728+ if [[ " $repo_created " -eq 0 ]]; then
729+ echo " "
730+ info " Create this repo on your personal GitHub before continuing:"
731+ info " https://github.com/new?name=$mirror_repo_name "
732+ echo " "
733+ info " Press Enter when done..."
734+ read -r
735+ fi
736+ elif command -v gh & > /dev/null && gh auth status & > /dev/null 2>&1 ; then
737+ # Fallback: use gh CLI (single account or HTTPS)
601738 if confirm " Create a new public mirror repo on GitHub?" ; then
602- mirror_repo_name=" $( prompt " Repo name" " work-contributions-mirror" ) "
603739 gh_user=" $( gh api user -q .login 2> /dev/null || echo " " ) "
604740 if [[ -n " $gh_user " ]]; then
605- # Check if gh is authenticated as work account (matches org or work username)
741+ # Check if gh is authenticated as work account
606742 is_work_account=0
607743 if [[ -n " ${org_name:- } " && " $gh_user " == " $org_name " ]]; then
608744 is_work_account=1
@@ -621,7 +757,6 @@ if [[ ! -d "$mirror_dir/.git" ]]; then
621757 skip_gh_create=1
622758 fi
623759 else
624- # Always confirm the target account before creating
625760 echo " "
626761 info " gh is authenticated as '$gh_user '."
627762 if ! confirm " Create repo under $gh_user ?" ; then
@@ -637,7 +772,6 @@ if [[ ! -d "$mirror_dir/.git" ]]; then
637772 if [[ -z " ${mirror_url:- } " && -z " ${skip_gh_create:- } " ]]; then
638773 if gh repo create " $mirror_repo_name " --public --description " Mirror of private work contributions" 2> /dev/null; then
639774 ok " Created repo on GitHub"
640- # Get clone URL matching auth method
641775 if [[ " $auth_method " == " ssh" ]]; then
642776 mirror_url=" $( gh repo view " $mirror_repo_name " --json sshUrl -q .sshUrl 2> /dev/null) "
643777 else
@@ -664,8 +798,11 @@ if [[ ! -d "$mirror_dir/.git" ]]; then
664798 mkdir -p " $mirror_dir "
665799 git -C " $mirror_dir " init --quiet
666800 git -C " $mirror_dir " remote add origin " $mirror_url "
667- git -C " $mirror_dir " commit --allow-empty -m " init" --quiet
668801 }
802+ # Ensure at least one commit exists (empty repos have no HEAD)
803+ if ! git -C " $mirror_dir " rev-parse --verify HEAD & > /dev/null; then
804+ git -C " $mirror_dir " commit --allow-empty -m " init" --quiet
805+ fi
669806 ok " Mirror repo ready at $mirror_dir "
670807
671808 # Test push access
@@ -677,58 +814,84 @@ if [[ ! -d "$mirror_dir/.git" ]]; then
677814 warn " Can't push to mirror repo. Commits will be created locally but won't appear on GitHub."
678815 echo " "
679816 if [[ " $mirror_url " == https://* ]]; then
680- info " HTTPS repos need authentication to push. Options:"
681- echo " "
682- info " Option 1: Embed a Personal Access Token in the URL"
683- info " ─────────────────────────────────────────────────"
684- info " Create a PAT on your PERSONAL GitHub account (not work):"
685- info " https://github.com/settings/tokens/new"
686- info " Scope: [x] repo"
687- echo " "
688- if confirm " Do you have a personal GitHub token for push access?" ; then
689- printf " Paste token (hidden): " >&2
690- read -rs mirror_token
691- echo " " >&2
692- else
693- echo " "
694- info " No worries — here's how to create one:"
695- echo " "
696- info " 1. Go to: https://github.com/settings/tokens/new"
697- info " (Make sure you're logged into your PERSONAL GitHub, not work)"
698- info " 2. Note: greens-push"
699- info " 3. Expiration: 90 days (or custom)"
700- info " 4. Select scopes:"
701- info " [x] repo"
702- info " 5. Click 'Generate token' and copy it"
703- echo " "
704- if confirm " Ready to paste the token?" ; then
705- printf " Paste token (hidden): " >&2
706- read -rs mirror_token
707- echo " " >&2
708- else
709- mirror_token=" "
710- fi
711- fi
712- if [[ -n " ${mirror_token:- } " ]]; then
713- # Embed token in remote URL: https://TOKEN@github.com/user/repo.git
714- authed_url=" $( echo " $mirror_url " | sed " s|https://|https://${mirror_token} @|" ) "
715- git -C " $mirror_dir " remote set-url origin " $authed_url "
716- if git -C " $mirror_dir " push origin HEAD 2> /dev/null; then
717- ok " Push access works now"
718- else
719- warn " Still can't push. Check that the token has repo scope and the repo exists on GitHub."
720- fi
721- else
722- warn " Skipping — your sync will run but commits won't push to GitHub."
723- info " Fix later: greens --setup"
724- fi
817+ fix_https_auth " $mirror_dir " " $mirror_url "
725818 else
726819 info " Check that your SSH key has access to push to this repo."
727820 fi
728821 fi
729822 fi
730823else
731824 ok " Mirror repo already exists at $mirror_dir "
825+ current_remote=" $( git -C " $mirror_dir " remote get-url origin 2> /dev/null || echo " " ) "
826+ if [[ -n " $current_remote " ]]; then
827+ info " Current remote: $( mask_url " $current_remote " ) "
828+ echo " "
829+
830+ # Test push access
831+ info " Testing push access..."
832+ if git -C " $mirror_dir " push origin HEAD 2> /dev/null; then
833+ ok " Push access works"
834+ else
835+ warn " Can't push to mirror repo."
836+ echo " "
837+ echo " What would you like to do?" >&2
838+ echo " 1) Change the remote URL (wrong repo or deleted repo)" >&2
839+ echo " 2) Fix push authentication (HTTPS token)" >&2
840+ echo " 3) Skip (fix later)" >&2
841+ printf " Choice [1]: " >&2
842+ read -r push_fix_choice
843+ push_fix_choice=" ${push_fix_choice:- 1} "
844+
845+ case " $push_fix_choice " in
846+ 1)
847+ new_mirror_url=" $( prompt " New mirror repo URL" " " ) "
848+ if [[ -n " $new_mirror_url " ]]; then
849+ git -C " $mirror_dir " remote set-url origin " $new_mirror_url "
850+ ok " Remote updated to $( mask_url " $new_mirror_url " ) "
851+ # Test push with new URL
852+ info " Testing push access..."
853+ if git -C " $mirror_dir " push origin HEAD 2> /dev/null; then
854+ ok " Push access works"
855+ elif [[ " $new_mirror_url " == https://* ]]; then
856+ warn " Can't push with new URL."
857+ fix_https_auth " $mirror_dir " " $new_mirror_url "
858+ else
859+ warn " Still can't push. Check SSH access to this repo."
860+ fi
861+ fi
862+ ;;
863+ 2)
864+ if [[ " $current_remote " == https://* ]]; then
865+ fix_https_auth " $mirror_dir " " $current_remote "
866+ else
867+ info " Current remote uses SSH. Check that your SSH key has push access."
868+ info " Test: ssh -T git@github.com"
869+ fi
870+ ;;
871+ * )
872+ info " Skipping. Fix later: greens --setup"
873+ ;;
874+ esac
875+ fi
876+ else
877+ warn " No remote URL configured for mirror repo."
878+ new_mirror_url=" $( prompt " Mirror repo URL" " " ) "
879+ if [[ -n " $new_mirror_url " ]]; then
880+ git -C " $mirror_dir " remote add origin " $new_mirror_url " 2> /dev/null || \
881+ git -C " $mirror_dir " remote set-url origin " $new_mirror_url "
882+ ok " Remote set to $( mask_url " $new_mirror_url " ) "
883+ # Test push access right away
884+ info " Testing push access..."
885+ if git -C " $mirror_dir " push origin HEAD 2> /dev/null; then
886+ ok " Push access works"
887+ elif [[ " $new_mirror_url " == https://* ]]; then
888+ warn " Can't push."
889+ fix_https_auth " $mirror_dir " " $new_mirror_url "
890+ else
891+ warn " Can't push. Check SSH access to this repo."
892+ fi
893+ fi
894+ fi
732895fi
733896
734897# ─────────────────────────────────────────────────────────────────────────────
0 commit comments