Skip to content

Instantly share code, notes, and snippets.

@sughodke
Last active November 1, 2025 21:59
Show Gist options
  • Select an option

  • Save sughodke/704015513d44fd15ef36acbe448472b5 to your computer and use it in GitHub Desktop.

Select an option

Save sughodke/704015513d44fd15ef36acbe448472b5 to your computer and use it in GitHub Desktop.
git file-pick -- Interactive file picker between Git branches using fzf
#!/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\""
@sughodke
Copy link
Author

sughodke commented Nov 1, 2025

git subcommand that lets you interactively pick specific files from one branch into another — directly from the terminal.

Requires fzf to show only relevant modified files and their diffs.

Installation

Move it to ~/.local/bin and it will be auto picked up by git.

git file-pick <source-branch> <target-branch>

# advanced: override base if needed
BASE_UPSTREAM=origin/develop git file-pick feature/payments feat/pick-payments

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment