Last active
November 1, 2025 21:59
-
-
Save sughodke/704015513d44fd15ef36acbe448472b5 to your computer and use it in GitHub Desktop.
git file-pick -- Interactive file picker between Git branches using fzf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # git-file-pick: interactively pick files from a source branch into a fresh or existing branch, | |
| # comparing from the fork point with origin/main, and excluding files already identical in target. | |
| set -euo pipefail | |
| # --- prerequisites --- | |
| need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }; } | |
| need git | |
| need fzf | |
| git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "Not inside a git repository." >&2; exit 1; } | |
| # --- args --- | |
| SOURCE_BRANCH="${1:-}" | |
| TARGET_BRANCH="${2:-}" | |
| BASE_UPSTREAM="${BASE_UPSTREAM:-origin/main}" # override with env var if needed | |
| if [[ -z "$SOURCE_BRANCH" || -z "$TARGET_BRANCH" ]]; then | |
| echo "Usage: git file-pick <source-branch> <target-branch>" >&2 | |
| echo " BASE_UPSTREAM=<remote/base> git pick <source> <target> # default: origin/main" >&2 | |
| exit 1 | |
| fi | |
| # --- ensure clean working tree --- | |
| if ! git diff --quiet || ! git diff --cached --quiet; then | |
| echo "Your working tree has changes. Commit or stash them first." >&2 | |
| exit 1 | |
| fi | |
| # --- ensure branches exist / updated --- | |
| if ! git rev-parse --verify --quiet "$SOURCE_BRANCH" >/dev/null; then | |
| echo "Source '$SOURCE_BRANCH' not found locally. Fetching..." >&2 | |
| git fetch --all --prune --quiet | |
| git rev-parse --verify --quiet "$SOURCE_BRANCH" >/dev/null || { | |
| echo "Source '$SOURCE_BRANCH' still not found." >&2 | |
| exit 1 | |
| } | |
| fi | |
| git fetch origin --prune --quiet || true | |
| git rev-parse --verify --quiet "$BASE_UPSTREAM" >/dev/null || { | |
| echo "Base upstream '$BASE_UPSTREAM' not found. Ensure the remote/branch exists." >&2 | |
| exit 1 | |
| } | |
| # --- fork point between base and source --- | |
| FORK_POINT="$(git merge-base --fork-point "$BASE_UPSTREAM" "$SOURCE_BRANCH" 2>/dev/null || true)" | |
| [[ -z "$FORK_POINT" ]] && FORK_POINT="$(git merge-base "$BASE_UPSTREAM" "$SOURCE_BRANCH")" | |
| [[ -z "$FORK_POINT" ]] && { echo "Could not determine fork point between '$BASE_UPSTREAM' and '$SOURCE_BRANCH'." >&2; exit 1; } | |
| # --- create or reuse target branch (from base) --- | |
| if git rev-parse --verify --quiet "$TARGET_BRANCH" >/dev/null; then | |
| echo "⚠️ Branch '$TARGET_BRANCH' already exists. Switching to it..." | |
| git switch "$TARGET_BRANCH" | |
| else | |
| echo "Creating new branch '$TARGET_BRANCH' from $BASE_UPSTREAM..." | |
| git switch -c "$TARGET_BRANCH" "$BASE_UPSTREAM" | |
| fi | |
| # --- candidate files: Added/Modified since fork point on source --- | |
| # (remove --diff-filter=AM if you want deletions/renames too) | |
| CHANGED_FILES="$(git diff --name-only --diff-filter=AM "${FORK_POINT}..${SOURCE_BRANCH}")" | |
| if [[ -z "${CHANGED_FILES}" ]]; then | |
| echo "No added/modified files on '$SOURCE_BRANCH' since its fork point with '$BASE_UPSTREAM'." >&2 | |
| exit 0 | |
| fi | |
| # --- exclude files already identical between target and source --- | |
| FILTERED_FILES="" | |
| while IFS= read -r f; do | |
| [[ -z "$f" ]] && continue | |
| # keep only if file differs between target and source | |
| if ! git diff --quiet "$TARGET_BRANCH" "$SOURCE_BRANCH" -- "$f"; then | |
| FILTERED_FILES+="$f"$'\n' | |
| fi | |
| done <<< "$CHANGED_FILES" | |
| if [[ -z "${FILTERED_FILES// }" ]]; then | |
| echo "All changed files from '$SOURCE_BRANCH' are already present in '$TARGET_BRANCH'. Nothing new to pick." >&2 | |
| exit 0 | |
| fi | |
| # --- interactive picker (TAB to mark, ENTER to accept) --- | |
| HEADER="TAB to select, ENTER to confirm | Diffs since fork $(git rev-parse --short "$FORK_POINT") vs $BASE_UPSTREAM" | |
| SELECTED="$(printf "%s" "$FILTERED_FILES" | \ | |
| fzf --multi \ | |
| --prompt="Pick files from $SOURCE_BRANCH → $TARGET_BRANCH > " \ | |
| --header="$HEADER" \ | |
| --preview-window=right:70%:wrap \ | |
| --preview="git diff --color=always ${FORK_POINT}..${SOURCE_BRANCH} -- {}")" | |
| if [[ -z "${SELECTED:-}" ]]; then | |
| echo "No files selected. Exiting." | |
| exit 0 | |
| fi | |
| # --- bring selected files from source into target (unstaged) --- | |
| while IFS= read -r path; do | |
| [[ -z "$path" ]] && continue | |
| git checkout "$SOURCE_BRANCH" -- "$path" | |
| done <<< "$SELECTED" | |
| echo | |
| echo "✅ Brought these files from '$SOURCE_BRANCH' into '$TARGET_BRANCH':" | |
| echo "$SELECTED" | |
| echo | |
| echo "Next:" | |
| echo " git status" | |
| echo " git add <files>" | |
| echo " git commit -m \"Pick files from $SOURCE_BRANCH since fork point\"" | |
| echo " git push -u origin \"$TARGET_BRANCH\"" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
gitsubcommand that lets you interactively pick specific files from one branch into another — directly from the terminal.Requires
fzfto show only relevant modified files and their diffs.Installation
Move it to ~/.local/bin and it will be auto picked up by git.
TAB key will mark/unmark files
ENTER will confirm selection
ESC / Ctrl+C to cancel
Can be re-run any number of times, will not abort if there's staged changes.