Initial commit
This commit is contained in:
88
CLAUDE.md
Normal file
88
CLAUDE.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# CLAUDE.md — Project Context for AI Assistants
|
||||
|
||||
## What This Project Is
|
||||
|
||||
A set of shell scripts for bidirectional git repository synchronization between
|
||||
two networks (home ↔ enterprise) that have **no direct network connection**.
|
||||
The transport medium is a **shared file system**, mounted at `~/fifiletrans` on
|
||||
both sides (path configurable via `$SYNC_SHARE` or as a CLI argument).
|
||||
|
||||
## Network Context
|
||||
|
||||
- **Home network**: has a **Gitea** instance (local git hosting + mirror/backup)
|
||||
- **Office network**: has a **GitLab** instance
|
||||
- There is **no direct sync** between Gitea and GitLab — bundle files via the
|
||||
shared filesystem are the only transport between the two networks
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Role |
|
||||
|---|---|
|
||||
| `sync-push.sh` | Incremental export: bundles new commits → shared folder |
|
||||
| `sync-pull.sh` | Import: picks a bundle from shared folder, fetches branches, integrates |
|
||||
| `sync-init-export.sh` | One-time bootstrap: full bundle + copies scripts → shared folder |
|
||||
| `sync-init-import.sh` | One-time bootstrap: clones from full bundle into a new directory |
|
||||
| `gitea-setup.sh` | Registers a local repo (git or unversioned) with the home Gitea instance |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **`git bundle`** chosen over `git format-patch`: handles multi-branch and
|
||||
arbitrary commit ranges in a single file; no dependency on branch topology.
|
||||
- **Incremental tracking** via `git config sync.exported.<branch>` (per branch,
|
||||
local git config). Avoids polluting branches/tags namespace.
|
||||
- **Incoming commits** fetched to `refs/sync/incoming/<branch>` — not merged
|
||||
automatically. User inspects and chooses ff / merge / rebase / skip.
|
||||
- **Semi-automated**: scripts triggered manually; minimal prompts only where
|
||||
a decision is needed (integration strategy, bundle selection).
|
||||
- **Multi-branch**: all scripts iterate all local branches. Checkpoints are
|
||||
independent per branch.
|
||||
- **Gitea auto-push** is opt-in (`git config sync.gitea.autopush true`), toggled
|
||||
per repo, and runs automatically at the end of `sync-push.sh` when enabled.
|
||||
- **Initial bootstrap** (`sync-init-export/import`) handles the case where the
|
||||
receiving machine has no directory at all — creates the repo from scratch and
|
||||
leaves both sides in identical state with checkpoints pre-set.
|
||||
|
||||
## Git Config Keys (all local, per repo)
|
||||
|
||||
| Key | Written by | Purpose |
|
||||
|---|---|---|
|
||||
| `sync.exported.<branch>` | `sync-push.sh` | Last exported commit hash per branch |
|
||||
| `sync.gitea.autopush` | `gitea-setup.sh` | `true`/`false` auto-push after bundle export |
|
||||
| `sync.gitea.remote` | `gitea-setup.sh` | Gitea remote name (default: `gitea`) |
|
||||
|
||||
Reset one branch checkpoint: `git config --unset sync.exported.<branch>`
|
||||
Reset all checkpoints: `git config --remove-section sync.exported`
|
||||
|
||||
## Bundle Filename Convention
|
||||
|
||||
```
|
||||
<repo-name>-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← ongoing sync
|
||||
<repo-name>-INIT-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← initial bootstrap
|
||||
```
|
||||
|
||||
Pull scripts filter by `<repo-name>` so multiple repos can share one folder.
|
||||
|
||||
## External Dependencies
|
||||
|
||||
- `git` (standard, all features used are in git ≥ 2.x)
|
||||
- `bash` ≥ 4 (for associative-array-free scripts; uses indexed arrays only)
|
||||
- `tea` (Gitea CLI) — only required for `gitea-setup.sh`
|
||||
- Standard POSIX tools: `find`, `awk`, `cut`, `du`, `hostname`, `date`
|
||||
|
||||
## Platform
|
||||
|
||||
Home side runs on **Linux / Raspberry Pi** (aarch64). Scripts use no
|
||||
architecture-specific features and should work on any POSIX system.
|
||||
|
||||
## When Modifying These Scripts
|
||||
|
||||
- Keep `set -euo pipefail` — fail fast is intentional.
|
||||
- The "nothing new to export" pre-check in `sync-push.sh` must run *before*
|
||||
`git bundle create` — git errors on an empty bundle.
|
||||
- `sync-pull.sh` must not auto-merge without user confirmation.
|
||||
- `sync-init-import.sh` must set `sync.exported.<branch>` checkpoints for all
|
||||
imported branches so the first `sync-push` from the new machine is incremental.
|
||||
- `gitea-setup.sh` parses `tea login list --output csv`; column order is:
|
||||
`NAME, URL, SSH-URL, USER, ACTIVE`. The active login has `true` in column 5.
|
||||
- Test each script against both first-run (no checkpoints) and incremental scenarios.
|
||||
- Test `gitea-setup.sh` against both: folder already a git repo, and raw unversioned folder.
|
||||
281
README.md
Normal file
281
README.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Git Bundle Sync — Air-Gap Repository Synchronization
|
||||
|
||||
Bidirectional git sync between a **home** and an **enterprise** network using a
|
||||
shared file system. No direct network connection between the two networks is
|
||||
required.
|
||||
|
||||
## Problem & Solution
|
||||
|
||||
Two git repositories on separate, non-routable networks need to stay in sync.
|
||||
The only common ground is a shared file system (mounted at `~/fifiletrans` on
|
||||
both sides by default).
|
||||
|
||||
Solution: `git bundle` — a built-in git feature that packs any range of commits
|
||||
into a single portable binary file. The bundle is written to the shared folder
|
||||
by one side and picked up by the other.
|
||||
|
||||
```
|
||||
Home repo ──── sync-push.sh ────► ~/fifiletrans/ ◄──── sync-pull.sh ──── Office repo
|
||||
Home repo ◄─── sync-pull.sh ──── ~/fifiletrans/ ◄──── sync-push.sh ──── Office repo
|
||||
```
|
||||
|
||||
**Network context:**
|
||||
- Home network has a **Gitea** instance — used as a local mirror/backup
|
||||
- Office network has a **GitLab** instance — no direct sync between the two is possible
|
||||
- Bundle files via shared filesystem are the only transport between the two networks
|
||||
|
||||
---
|
||||
|
||||
## Scripts Overview
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `sync-push.sh` | Export new commits as a bundle into the shared folder |
|
||||
| `sync-pull.sh` | Import a bundle from the shared folder and integrate it |
|
||||
| `sync-init-export.sh` | **One-time**: create full bootstrap bundle + copy scripts for new machine |
|
||||
| `sync-init-import.sh` | **One-time**: clone from bootstrap bundle into a brand-new directory |
|
||||
| `gitea-setup.sh` | Initialize a local folder (git or not) and push it to Gitea |
|
||||
|
||||
---
|
||||
|
||||
## Initial Setup of a New Machine
|
||||
|
||||
Use this when setting up the repo on a machine for the first time (no existing
|
||||
directory, no git history).
|
||||
|
||||
### On the source machine
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repo
|
||||
./sync-init-export.sh
|
||||
```
|
||||
|
||||
This places in `~/fifiletrans/`:
|
||||
- `<repo>-INIT-from-<host>-<timestamp>.bundle` — complete git history
|
||||
- `sync-push.sh`, `sync-pull.sh`, `sync-init-import.sh` — scripts for the other side
|
||||
- `INIT-HOWTO-<repo>.txt` — printed instructions
|
||||
|
||||
### On the receiving machine
|
||||
|
||||
```bash
|
||||
bash ~/fifiletrans/sync-init-import.sh ~/fifiletrans /path/to/new/repo
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Clone the full bundle into `/path/to/new/repo` (directory must not exist)
|
||||
2. Check out all branches
|
||||
3. Install `sync-push.sh` and `sync-pull.sh` into the new repo
|
||||
4. Set export checkpoints so the first `sync-push` is incremental
|
||||
|
||||
After this, **both machines are in sync** and ready for the ongoing workflow.
|
||||
|
||||
---
|
||||
|
||||
## Ongoing Sync Workflow
|
||||
|
||||
### Send changes (on either machine)
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repo
|
||||
./sync-push.sh
|
||||
```
|
||||
|
||||
Creates an incremental bundle (only commits since last push). First run after
|
||||
init creates a full bundle. Output example:
|
||||
|
||||
```
|
||||
Repository : myrepo
|
||||
Host : homepc
|
||||
Branches : main
|
||||
Mode : incremental
|
||||
New commits: 3
|
||||
main: 3 new commit(s)
|
||||
a1b2c3d Add feature X
|
||||
e4f5g6h Fix bug in Y
|
||||
i7j8k9l Refactor Z
|
||||
|
||||
Bundle created : /home/user/fifiletrans/myrepo-from-homepc-20260221-143022.bundle (12K)
|
||||
```
|
||||
|
||||
### Receive changes (on the other machine)
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repo
|
||||
./sync-pull.sh
|
||||
```
|
||||
|
||||
The script:
|
||||
1. Lists all available bundles for this repo in the shared folder
|
||||
2. Defaults to the newest one (press Enter to accept)
|
||||
3. Verifies bundle integrity
|
||||
4. Shows exactly which commits are incoming and whether branches have diverged
|
||||
5. Offers **fast-forward**, **merge**, **rebase**, or **skip** per branch
|
||||
|
||||
### Typical round-trip
|
||||
|
||||
```bash
|
||||
# ── At home, after finishing work ──────────────────────────────────────────
|
||||
cd ~/projects/myrepo
|
||||
git commit -m "..."
|
||||
./sync-push.sh
|
||||
# → writes ~/fifiletrans/myrepo-from-homepc-TIMESTAMP.bundle
|
||||
|
||||
# ── At office, pick up home changes ────────────────────────────────────────
|
||||
cd ~/projects/myrepo
|
||||
./sync-pull.sh
|
||||
# → lists bundles, shows new commits, fast-forwards main
|
||||
|
||||
# ── At office, continue working ────────────────────────────────────────────
|
||||
git commit -m "..."
|
||||
./sync-push.sh
|
||||
# → writes ~/fifiletrans/myrepo-from-officepc-TIMESTAMP.bundle
|
||||
|
||||
# ── At home, pick up office changes ────────────────────────────────────────
|
||||
./sync-pull.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gitea Integration (Home Network)
|
||||
|
||||
`gitea-setup.sh` registers a local repository with the home Gitea instance.
|
||||
Works for both cases:
|
||||
- **Folder already a git repo** — creates Gitea repo and pushes
|
||||
- **Folder not yet versioned** — runs `git init`, stages all files, commits, then pushes
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install tea (Gitea CLI) if not already present
|
||||
# See https://gitea.com/gitea/tea/releases
|
||||
|
||||
# Configure your Gitea login (once per machine)
|
||||
tea login add
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
./gitea-setup.sh [options] [<repo-path>]
|
||||
|
||||
Options:
|
||||
-n, --name NAME Gitea repo name (default: directory name)
|
||||
-d, --desc DESC Repository description
|
||||
-o, --org ORG Create under organization
|
||||
-p, --private Create as private repo
|
||||
-l, --login LOGIN tea login profile (default: active login)
|
||||
--auto-push Enable Gitea auto-push after sync-push.sh
|
||||
--no-auto-push Disable Gitea auto-push (default)
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
# New, unversioned project
|
||||
./gitea-setup.sh --name my-project --private ~/projects/my-project
|
||||
|
||||
# Existing git repo, enable auto-push to Gitea
|
||||
cd ~/projects/existing-repo
|
||||
./gitea-setup.sh --auto-push
|
||||
```
|
||||
|
||||
### Gitea Auto-Push
|
||||
|
||||
When enabled, every `sync-push.sh` run also pushes all branches and tags to the
|
||||
Gitea remote automatically.
|
||||
|
||||
```bash
|
||||
# Enable
|
||||
git config sync.gitea.autopush true
|
||||
|
||||
# Disable
|
||||
git config sync.gitea.autopush false
|
||||
|
||||
# Check status
|
||||
git config sync.gitea.autopush
|
||||
```
|
||||
|
||||
The Gitea remote name defaults to `gitea`. Set a different one with:
|
||||
```bash
|
||||
git config sync.gitea.remote <remote-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
All settings are stored in the repo's local git config (`.git/config`).
|
||||
|
||||
| Key | Set by | Purpose |
|
||||
|---|---|---|
|
||||
| `sync.exported.<branch>` | `sync-push.sh` | Last exported commit hash per branch |
|
||||
| `sync.gitea.autopush` | `gitea-setup.sh` | `true`/`false` — auto-push to Gitea after bundle export |
|
||||
| `sync.gitea.remote` | `gitea-setup.sh` | Name of the Gitea git remote (default: `gitea`) |
|
||||
|
||||
---
|
||||
|
||||
## Bundle Filename Convention
|
||||
|
||||
```
|
||||
<repo-name>-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← ongoing sync
|
||||
<repo-name>-INIT-from-<hostname>-<YYYYMMDD-HHMMSS>.bundle ← initial bootstrap
|
||||
```
|
||||
|
||||
The pull scripts filter by `<repo-name>` automatically, so multiple repos can
|
||||
safely share the same folder.
|
||||
|
||||
---
|
||||
|
||||
## Multiple Branches
|
||||
|
||||
All scripts handle multiple branches automatically:
|
||||
|
||||
- `sync-push.sh` / `sync-init-export.sh` — export all local branches; incremental
|
||||
checkpoint is tracked independently per branch
|
||||
- `sync-pull.sh` / `sync-init-import.sh` — import all branches from the bundle
|
||||
and prompt for integration of each one individually
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
**Apply incremental bundles in order.**
|
||||
An incremental bundle depends on commits already present in the receiving repo.
|
||||
If you skip a bundle, the next pull will fail verification. Apply bundles in
|
||||
chronological order (oldest timestamp first) when catching up after a gap.
|
||||
|
||||
**Checkpoints are local and per-machine.**
|
||||
`git config sync.exported.<branch>` tracks what *this* machine has sent. It is
|
||||
not shared and not affected by imports.
|
||||
|
||||
**Incoming refs persist.**
|
||||
Fetched commits are stored under `refs/sync/incoming/<branch>` and remain
|
||||
available between pull runs. Inspect them anytime:
|
||||
|
||||
```bash
|
||||
git log refs/sync/incoming/main
|
||||
git diff main refs/sync/incoming/main
|
||||
```
|
||||
|
||||
**Conflict resolution.**
|
||||
When branches have diverged and you choose merge or rebase, the script hands
|
||||
off to standard git. Resolve conflicts normally, then:
|
||||
```bash
|
||||
git rebase --continue # or
|
||||
git merge --continue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| "Bundle verification failed" | Incremental bundle missing prerequisites | Apply missing earlier bundles first, or force full re-export (see below) |
|
||||
| "Nothing new to export" | No commits since last push on any branch | Nothing to do |
|
||||
| No bundles listed by sync-pull | Bundle filename mismatch (repo dir name differs) | Check repo directory name matches on both machines |
|
||||
| Gitea push fails | Remote URL wrong or auth issue | `git remote -v` and `tea login list` to verify |
|
||||
| `tea: command not found` | Gitea CLI not installed | Install from https://gitea.com/gitea/tea/releases |
|
||||
| Want to reset one branch checkpoint | — | `git config --unset sync.exported.<branch>` |
|
||||
| Want to force a full re-export | — | `git config --remove-section sync.exported` |
|
||||
| Skipped integration, want to redo | — | Run `./sync-pull.sh` again and pick the same bundle |
|
||||
212
gitea-setup.sh
Executable file
212
gitea-setup.sh
Executable file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
# gitea-setup.sh — Initialize a local folder as a git repo and push to Gitea.
|
||||
#
|
||||
# Handles two cases:
|
||||
# A) Folder is already a git repo → just create Gitea repo and push
|
||||
# B) Folder is not yet a git repo → git init, stage all, commit, then push
|
||||
#
|
||||
# Usage:
|
||||
# ./gitea-setup.sh [options] [<repo-path>]
|
||||
#
|
||||
# Options:
|
||||
# -n, --name NAME Gitea repo name (default: directory name)
|
||||
# -d, --desc DESC Repository description (default: empty)
|
||||
# -o, --org ORG Create under organization instead of personal account
|
||||
# -p, --private Create as private repo
|
||||
# -l, --login LOGIN tea login profile (default: active login)
|
||||
# --auto-push Enable Gitea auto-push after sync-push.sh
|
||||
# --no-auto-push Disable Gitea auto-push (default)
|
||||
# -h, --help Show this help
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Gitea CLI (tea) installed and at least one login configured
|
||||
# (run 'tea login add' if not done yet)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
REPO_PATH="."
|
||||
REPO_NAME=""
|
||||
DESCRIPTION=""
|
||||
ORG=""
|
||||
PRIVATE=false
|
||||
TEA_LOGIN_FLAG=""
|
||||
AUTO_PUSH=false
|
||||
AUTO_PUSH_SET=false
|
||||
|
||||
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--name) REPO_NAME="$2"; shift 2 ;;
|
||||
-d|--desc) DESCRIPTION="$2"; shift 2 ;;
|
||||
-o|--org) ORG="$2"; shift 2 ;;
|
||||
-p|--private) PRIVATE=true; shift ;;
|
||||
-l|--login) TEA_LOGIN_FLAG="--login $2"; shift 2 ;;
|
||||
--auto-push) AUTO_PUSH=true; AUTO_PUSH_SET=true; shift ;;
|
||||
--no-auto-push) AUTO_PUSH=false; AUTO_PUSH_SET=true; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,/^set /p' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
|
||||
exit 0 ;;
|
||||
-*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
*) REPO_PATH="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Sanity checks ─────────────────────────────────────────────────────────────
|
||||
if ! command -v tea &>/dev/null; then
|
||||
echo "ERROR: 'tea' (Gitea CLI) is not installed or not in PATH." >&2
|
||||
echo " Install it from https://gitea.com/gitea/tea/releases" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! tea login list &>/dev/null || [ "$(tea login list --output csv 2>/dev/null | wc -l)" -lt 2 ]; then
|
||||
echo "ERROR: No Gitea login configured. Run: tea login add" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve absolute path (directory may not exist yet — only warn)
|
||||
REPO_PATH="$(realpath -m "$REPO_PATH")"
|
||||
if [ ! -d "$REPO_PATH" ]; then
|
||||
echo "Directory does not exist, creating: $REPO_PATH"
|
||||
mkdir -p "$REPO_PATH"
|
||||
fi
|
||||
|
||||
[ -z "$REPO_NAME" ] && REPO_NAME="$(basename "$REPO_PATH")"
|
||||
|
||||
# ── Detect active tea login info ──────────────────────────────────────────────
|
||||
# CSV columns: NAME,URL,SSH-URL,USER,ACTIVE
|
||||
ACTIVE_LOGIN_LINE=$(tea login list --output csv 2>/dev/null \
|
||||
| awk -F',' 'NR>1 && $5=="true" {print; exit}')
|
||||
|
||||
if [ -z "$ACTIVE_LOGIN_LINE" ]; then
|
||||
# Fallback: take first non-header line
|
||||
ACTIVE_LOGIN_LINE=$(tea login list --output csv 2>/dev/null \
|
||||
| awk -F',' 'NR==2 {print; exit}')
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE_LOGIN_LINE" ]; then
|
||||
echo "ERROR: Could not determine active Gitea login. Run: tea login list" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GITEA_URL=$(echo "$ACTIVE_LOGIN_LINE" | cut -d',' -f2 | tr -d '"' | sed 's|/$||')
|
||||
GITEA_USER=$(echo "$ACTIVE_LOGIN_LINE" | cut -d',' -f4 | tr -d '"')
|
||||
|
||||
echo "Gitea instance : $GITEA_URL"
|
||||
echo "Gitea user : $GITEA_USER"
|
||||
echo "Repo name : $REPO_NAME"
|
||||
echo "Path : $REPO_PATH"
|
||||
[ -n "$ORG" ] && echo "Organization : $ORG"
|
||||
$PRIVATE && echo "Visibility : private" || echo "Visibility : public"
|
||||
echo ""
|
||||
|
||||
# ── Step 1: Ensure it is a git repo ───────────────────────────────────────────
|
||||
cd "$REPO_PATH"
|
||||
|
||||
if git rev-parse --git-dir &>/dev/null; then
|
||||
echo "[1/4] Already a git repository — skipping init."
|
||||
IS_NEW_REPO=false
|
||||
else
|
||||
echo "[1/4] Initializing git repository..."
|
||||
git init
|
||||
git checkout -b main 2>/dev/null || true
|
||||
|
||||
# Stage everything
|
||||
git add .
|
||||
STAGED=$(git diff --cached --name-only | wc -l)
|
||||
if [ "$STAGED" -gt 0 ]; then
|
||||
echo " Staging $STAGED file(s)..."
|
||||
read -rp " Commit message [Initial commit]: " MSG
|
||||
MSG="${MSG:-Initial commit}"
|
||||
git commit -m "$MSG"
|
||||
else
|
||||
echo " Directory is empty — creating an empty initial commit."
|
||||
git commit --allow-empty -m "Initial commit"
|
||||
fi
|
||||
IS_NEW_REPO=true
|
||||
fi
|
||||
|
||||
# ── Step 2: Create repo on Gitea ───────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[2/4] Creating Gitea repository '$REPO_NAME'..."
|
||||
|
||||
PRIVATE_FLAG=""
|
||||
$PRIVATE && PRIVATE_FLAG="--private"
|
||||
|
||||
ORG_FLAG=""
|
||||
[ -n "$ORG" ] && ORG_FLAG="--owner $ORG"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if ! tea repo create \
|
||||
--name "$REPO_NAME" \
|
||||
--description "$DESCRIPTION" \
|
||||
$PRIVATE_FLAG \
|
||||
$ORG_FLAG \
|
||||
$TEA_LOGIN_FLAG; then
|
||||
echo ""
|
||||
echo "ERROR: Failed to create Gitea repository." >&2
|
||||
echo " The repo may already exist. To push to an existing repo," >&2
|
||||
echo " add the remote manually and push:" >&2
|
||||
if [ -n "$ORG" ]; then
|
||||
echo " git remote add gitea ${GITEA_URL}/${ORG}/${REPO_NAME}.git" >&2
|
||||
else
|
||||
echo " git remote add gitea ${GITEA_URL}/${GITEA_USER}/${REPO_NAME}.git" >&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct remote URL
|
||||
if [ -n "$ORG" ]; then
|
||||
REMOTE_URL="${GITEA_URL}/${ORG}/${REPO_NAME}.git"
|
||||
else
|
||||
REMOTE_URL="${GITEA_URL}/${GITEA_USER}/${REPO_NAME}.git"
|
||||
fi
|
||||
|
||||
# ── Step 3: Add remote and push ────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[3/4] Adding remote 'gitea' → $REMOTE_URL"
|
||||
|
||||
if git remote get-url gitea &>/dev/null; then
|
||||
echo " Remote 'gitea' already exists — updating URL."
|
||||
git remote set-url gitea "$REMOTE_URL"
|
||||
else
|
||||
git remote add gitea "$REMOTE_URL"
|
||||
fi
|
||||
|
||||
echo " Pushing all branches and tags..."
|
||||
git push gitea --all
|
||||
git push gitea --tags
|
||||
|
||||
# ── Step 4: Configure auto-push ───────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[4/4] Configuring sync settings..."
|
||||
git config sync.gitea.remote "gitea"
|
||||
|
||||
if $AUTO_PUSH_SET; then
|
||||
git config sync.gitea.autopush "$AUTO_PUSH"
|
||||
$AUTO_PUSH \
|
||||
&& echo " Auto-push to Gitea after sync-push.sh: ENABLED" \
|
||||
|| echo " Auto-push to Gitea after sync-push.sh: DISABLED"
|
||||
else
|
||||
# Ask interactively
|
||||
read -rp " Enable automatic Gitea push after sync-push.sh? [y/N] " ans
|
||||
ans="${ans:-N}"
|
||||
if [[ "$ans" =~ ^[Yy] ]]; then
|
||||
git config sync.gitea.autopush "true"
|
||||
echo " Auto-push enabled. Toggle later with:"
|
||||
echo " git config sync.gitea.autopush false"
|
||||
else
|
||||
git config sync.gitea.autopush "false"
|
||||
echo " Auto-push disabled. Enable later with:"
|
||||
echo " git config sync.gitea.autopush true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Done. Repository is live at:"
|
||||
echo " $REMOTE_URL"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " - Run sync-push.sh to export commits to the shared folder"
|
||||
echo " - Run sync-init-export.sh to prepare a bootstrap bundle for the other machine"
|
||||
117
sync-init-export.sh
Executable file
117
sync-init-export.sh
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-init-export.sh — Create a full bootstrap bundle so the other machine can
|
||||
# clone the repo from scratch via the shared folder.
|
||||
#
|
||||
# Usage: ./sync-init-export.sh [<share-dir>]
|
||||
# Default share dir: $SYNC_SHARE or ~/fifiletrans
|
||||
#
|
||||
# Places in the shared folder:
|
||||
# <repo>-INIT-from-<host>-<timestamp>.bundle complete git history, all branches
|
||||
# sync-push.sh (copy of this repo's sync scripts)
|
||||
# sync-pull.sh
|
||||
# sync-init-import.sh
|
||||
#
|
||||
# The receiving side runs sync-init-import.sh from the shared folder.
|
||||
# After that, both sides are identical and ready to use sync-push / sync-pull.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
SHARE_DIR="${1:-${SYNC_SHARE:-$HOME/fifiletrans}}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Sanity checks ─────────────────────────────────────────────────────────────
|
||||
if ! git rev-parse --git-dir &>/dev/null; then
|
||||
echo "ERROR: Not inside a git repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
REPO_NAME=$(basename "$REPO_ROOT")
|
||||
HOST=$(hostname -s)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
|
||||
BUNDLE_FILE="${REPO_NAME}-INIT-from-${HOST}-${TIMESTAMP}.bundle"
|
||||
BUNDLE_PATH="${SHARE_DIR}/${BUNDLE_FILE}"
|
||||
|
||||
mkdir -p "$SHARE_DIR"
|
||||
|
||||
# ── Warn if an INIT bundle already exists for this repo ───────────────────────
|
||||
EXISTING=$(find "$SHARE_DIR" -maxdepth 1 -name "${REPO_NAME}-INIT-from-*.bundle" 2>/dev/null | sort)
|
||||
if [ -n "$EXISTING" ]; then
|
||||
echo "Warning: existing INIT bundle(s) found in $SHARE_DIR:"
|
||||
echo "$EXISTING" | sed 's/^/ /'
|
||||
echo ""
|
||||
read -rp "Continue and create a new one? [y/N] " ans
|
||||
ans="${ans:-N}"
|
||||
[[ "$ans" =~ ^[Yy] ]] || exit 0
|
||||
fi
|
||||
|
||||
# ── Show what will be bundled ──────────────────────────────────────────────────
|
||||
echo "Repository : $REPO_NAME"
|
||||
echo "Host : $HOST"
|
||||
echo "Mode : FULL (all branches, complete history)"
|
||||
echo ""
|
||||
|
||||
BRANCHES=()
|
||||
while IFS= read -r b; do
|
||||
BRANCHES+=("$b")
|
||||
done < <(git branch --format='%(refname:short)')
|
||||
|
||||
for branch in "${BRANCHES[@]}"; do
|
||||
count=$(git log --oneline "$branch" | wc -l)
|
||||
tip=$(git log -1 --format="%h %s" "$branch")
|
||||
echo " $branch : $count commit(s) [tip: $tip]"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ── Create the full bundle ────────────────────────────────────────────────────
|
||||
echo "Creating bundle..."
|
||||
git bundle create "$BUNDLE_PATH" --all
|
||||
|
||||
SIZE=$(du -sh "$BUNDLE_PATH" | cut -f1)
|
||||
echo "Bundle : $BUNDLE_PATH ($SIZE)"
|
||||
|
||||
# ── Copy sync scripts into the shared folder ──────────────────────────────────
|
||||
echo ""
|
||||
echo "Copying sync scripts to shared folder..."
|
||||
|
||||
SCRIPTS=("sync-push.sh" "sync-pull.sh" "sync-init-import.sh")
|
||||
for script in "${SCRIPTS[@]}"; do
|
||||
src="${SCRIPT_DIR}/${script}"
|
||||
if [ -f "$src" ]; then
|
||||
cp "$src" "${SHARE_DIR}/${script}"
|
||||
chmod +x "${SHARE_DIR}/${script}"
|
||||
echo " Copied : $script"
|
||||
else
|
||||
echo " WARNING: $script not found at $src — skipping." >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Write a HOWTO marker file ─────────────────────────────────────────────────
|
||||
HOWTO="${SHARE_DIR}/INIT-HOWTO-${REPO_NAME}.txt"
|
||||
cat > "$HOWTO" <<EOF
|
||||
Bootstrap instructions for: $REPO_NAME
|
||||
Generated : $(date)
|
||||
From host : $HOST
|
||||
Bundle : $BUNDLE_FILE
|
||||
|
||||
On the receiving machine:
|
||||
1. Make sure this folder ($(basename "$SHARE_DIR")) is accessible at the same path,
|
||||
or adjust the path in the command below.
|
||||
|
||||
2. Run:
|
||||
bash ${SHARE_DIR}/sync-init-import.sh ${SHARE_DIR} /path/to/new/repo
|
||||
|
||||
3. After the import completes, both machines are in sync.
|
||||
Use sync-push.sh and sync-pull.sh for ongoing updates.
|
||||
EOF
|
||||
echo " Written : $(basename "$HOWTO")"
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Shared folder contents:"
|
||||
ls -lh "$SHARE_DIR" | grep -E "(${REPO_NAME}|sync-)" | sed 's/^/ /'
|
||||
echo ""
|
||||
echo "On the other machine, run:"
|
||||
echo " bash ${SHARE_DIR}/sync-init-import.sh ${SHARE_DIR} /path/to/new/$(basename "$REPO_ROOT")"
|
||||
180
sync-init-import.sh
Executable file
180
sync-init-import.sh
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-init-import.sh — Bootstrap a repository from scratch using an INIT bundle
|
||||
# placed in the shared folder by sync-init-export.sh.
|
||||
#
|
||||
# Usage: ./sync-init-import.sh [<share-dir>] [<target-dir>]
|
||||
#
|
||||
# <share-dir> Shared folder containing the bundle (default: $SYNC_SHARE or ~/fifiletrans)
|
||||
# <target-dir> Where to create the new repo (default: prompted interactively)
|
||||
#
|
||||
# After completion:
|
||||
# - A fully functional git repo exists at <target-dir>
|
||||
# - All branches and tags from the bundle are present
|
||||
# - sync-push.sh and sync-pull.sh are copied into <target-dir>
|
||||
# - Export checkpoints are set so the first sync-push is incremental
|
||||
# - Both machines are now in sync
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
SHARE_DIR="${1:-${SYNC_SHARE:-$HOME/fifiletrans}}"
|
||||
TARGET_DIR="${2:-}"
|
||||
|
||||
# ── Sanity checks ─────────────────────────────────────────────────────────────
|
||||
if [ ! -d "$SHARE_DIR" ]; then
|
||||
echo "ERROR: Share directory not found: $SHARE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Find INIT bundles ─────────────────────────────────────────────────────────
|
||||
BUNDLES=()
|
||||
while IFS= read -r f; do
|
||||
BUNDLES+=("$f")
|
||||
done < <(find "$SHARE_DIR" -maxdepth 1 -name "*-INIT-from-*.bundle" | sort)
|
||||
|
||||
if [ ${#BUNDLES[@]} -eq 0 ]; then
|
||||
echo "ERROR: No INIT bundle found in $SHARE_DIR" >&2
|
||||
echo " (looking for: *-INIT-from-*.bundle)" >&2
|
||||
echo " Run sync-init-export.sh on the source machine first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Let user pick a bundle ─────────────────────────────────────────────────────
|
||||
echo "Available INIT bundles:"
|
||||
echo ""
|
||||
for i in "${!BUNDLES[@]}"; do
|
||||
b="${BUNDLES[$i]}"
|
||||
size=$(du -sh "$b" | cut -f1)
|
||||
name=$(basename "$b")
|
||||
echo " [$((i+1))] $name ($size)"
|
||||
done
|
||||
echo ""
|
||||
|
||||
DEFAULT=${#BUNDLES[@]}
|
||||
read -rp "Pick bundle [1-${#BUNDLES[@]}, default $DEFAULT]: " CHOICE
|
||||
CHOICE="${CHOICE:-$DEFAULT}"
|
||||
|
||||
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "${#BUNDLES[@]}" ]; then
|
||||
echo "Invalid choice." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUNDLE_PATH="${BUNDLES[$((CHOICE-1))]}"
|
||||
BUNDLE_NAME=$(basename "$BUNDLE_PATH")
|
||||
|
||||
# Derive a default target dir name from bundle name: <repo>-INIT-from-...
|
||||
REPO_NAME=$(echo "$BUNDLE_NAME" | sed 's/-INIT-from-.*//')
|
||||
|
||||
echo ""
|
||||
echo "Using bundle : $BUNDLE_NAME"
|
||||
|
||||
# ── Determine target directory ─────────────────────────────────────────────────
|
||||
if [ -z "$TARGET_DIR" ]; then
|
||||
read -rp "Target directory [$HOME/$REPO_NAME]: " TARGET_DIR
|
||||
TARGET_DIR="${TARGET_DIR:-$HOME/$REPO_NAME}"
|
||||
fi
|
||||
|
||||
TARGET_DIR="$(realpath -m "$TARGET_DIR")"
|
||||
echo "Target dir : $TARGET_DIR"
|
||||
echo ""
|
||||
|
||||
if [ -e "$TARGET_DIR" ]; then
|
||||
echo "ERROR: Target already exists: $TARGET_DIR" >&2
|
||||
echo " Remove it first or choose a different path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Verify the bundle ──────────────────────────────────────────────────────────
|
||||
echo "Verifying bundle..."
|
||||
# INIT bundles are full — they have no prerequisites, so verify should always pass
|
||||
git bundle verify "$BUNDLE_PATH"
|
||||
echo ""
|
||||
|
||||
# ── List branches in bundle ────────────────────────────────────────────────────
|
||||
echo "Branches in bundle:"
|
||||
BUNDLE_BRANCHES=()
|
||||
DEFAULT_BRANCH=""
|
||||
while IFS= read -r line; do
|
||||
ref=$(echo "$line" | awk '{print $2}')
|
||||
if [[ "$ref" == refs/heads/* ]]; then
|
||||
branch="${ref#refs/heads/}"
|
||||
BUNDLE_BRANCHES+=("$branch")
|
||||
echo " $branch"
|
||||
# Prefer 'main', then 'master' as default checkout
|
||||
if [ -z "$DEFAULT_BRANCH" ] || [ "$branch" = "main" ] || \
|
||||
{ [ "$DEFAULT_BRANCH" != "main" ] && [ "$branch" = "master" ]; }; then
|
||||
DEFAULT_BRANCH="$branch"
|
||||
fi
|
||||
fi
|
||||
done < <(git bundle list-heads "$BUNDLE_PATH")
|
||||
|
||||
if [ ${#BUNDLE_BRANCHES[@]} -eq 0 ]; then
|
||||
echo "ERROR: No branches found in bundle." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Will check out: $DEFAULT_BRANCH"
|
||||
echo ""
|
||||
|
||||
# ── Create and populate the repository ───────────────────────────────────────
|
||||
echo "[1/4] Initializing repository at $TARGET_DIR..."
|
||||
mkdir -p "$TARGET_DIR"
|
||||
cd "$TARGET_DIR"
|
||||
git init
|
||||
git checkout -b "$DEFAULT_BRANCH" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "[2/4] Fetching all branches and tags from bundle..."
|
||||
git fetch "$BUNDLE_PATH" 'refs/heads/*:refs/heads/*' 'refs/tags/*:refs/tags/*'
|
||||
|
||||
# Check out the default branch (fetch leaves us detached or on empty branch)
|
||||
git checkout "$DEFAULT_BRANCH"
|
||||
|
||||
# Report what we got
|
||||
echo ""
|
||||
echo " Branches:"
|
||||
git branch | sed 's/^/ /'
|
||||
echo " Tags:"
|
||||
git tag | sed 's/^/ /' || echo " (none)"
|
||||
|
||||
# ── Copy sync scripts into the new repo ───────────────────────────────────────
|
||||
echo ""
|
||||
echo "[3/4] Installing sync scripts..."
|
||||
|
||||
SCRIPTS=("sync-push.sh" "sync-pull.sh")
|
||||
for script in "${SCRIPTS[@]}"; do
|
||||
src="${SHARE_DIR}/${script}"
|
||||
if [ -f "$src" ]; then
|
||||
cp "$src" "${TARGET_DIR}/${script}"
|
||||
chmod +x "${TARGET_DIR}/${script}"
|
||||
echo " Installed : $script"
|
||||
else
|
||||
echo " WARNING: $script not found in $SHARE_DIR — copy it manually." >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Set export checkpoints so first sync-push is incremental ──────────────────
|
||||
echo ""
|
||||
echo "[4/4] Setting sync checkpoints..."
|
||||
for branch in "${BUNDLE_BRANCHES[@]}"; do
|
||||
tip=$(git rev-parse "$branch")
|
||||
git config "sync.exported.${branch}" "$tip"
|
||||
echo " sync.exported.$branch = ${tip:0:8}..."
|
||||
done
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════════════"
|
||||
echo " Repository ready at: $TARGET_DIR"
|
||||
echo "══════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Both machines are now in sync."
|
||||
echo ""
|
||||
echo "Ongoing workflow:"
|
||||
echo " cd $TARGET_DIR"
|
||||
echo " ./sync-push.sh # after committing — export changes"
|
||||
echo " ./sync-pull.sh # to receive changes from the other machine"
|
||||
echo ""
|
||||
echo "Tip: verify everything looks correct:"
|
||||
echo " cd $TARGET_DIR && git log --oneline --graph --all"
|
||||
213
sync-pull.sh
Executable file
213
sync-pull.sh
Executable file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-pull.sh — Import a git bundle from the shared folder into the current repo.
|
||||
#
|
||||
# Usage: ./sync-pull.sh [<share-dir>]
|
||||
# Default share dir: $SYNC_SHARE or ~/fifiletrans
|
||||
#
|
||||
# The script lists available bundles for this repo, lets you choose one,
|
||||
# fetches all branches from it into refs/sync/incoming/<branch>, shows
|
||||
# what's new, and offers to merge or rebase each branch.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
SHARE_DIR="${1:-${SYNC_SHARE:-$HOME/fifiletrans}}"
|
||||
|
||||
# ── Sanity checks ─────────────────────────────────────────────────────────────
|
||||
if ! git rev-parse --git-dir &>/dev/null; then
|
||||
echo "ERROR: Not inside a git repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$SHARE_DIR" ]; then
|
||||
echo "ERROR: Share directory not found: $SHARE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
REPO_NAME=$(basename "$REPO_ROOT")
|
||||
|
||||
# ── Find bundles for this repo ────────────────────────────────────────────────
|
||||
BUNDLES=()
|
||||
while IFS= read -r f; do
|
||||
BUNDLES+=("$f")
|
||||
done < <(find "$SHARE_DIR" -maxdepth 1 -name "${REPO_NAME}-from-*.bundle" | sort)
|
||||
|
||||
if [ ${#BUNDLES[@]} -eq 0 ]; then
|
||||
echo "No bundles found for repo '$REPO_NAME' in $SHARE_DIR"
|
||||
echo " (looking for: ${REPO_NAME}-from-*.bundle)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Let user pick a bundle ─────────────────────────────────────────────────────
|
||||
echo "Available bundles for '$REPO_NAME':"
|
||||
echo ""
|
||||
for i in "${!BUNDLES[@]}"; do
|
||||
b="${BUNDLES[$i]}"
|
||||
size=$(du -sh "$b" | cut -f1)
|
||||
name=$(basename "$b")
|
||||
echo " [$((i+1))] $name ($size)"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Default: newest bundle (last in sorted list)
|
||||
DEFAULT=${#BUNDLES[@]}
|
||||
read -rp "Pick bundle [1-${#BUNDLES[@]}, default $DEFAULT]: " CHOICE
|
||||
CHOICE="${CHOICE:-$DEFAULT}"
|
||||
|
||||
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ "$CHOICE" -lt 1 ] || [ "$CHOICE" -gt "${#BUNDLES[@]}" ]; then
|
||||
echo "Invalid choice." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUNDLE_PATH="${BUNDLES[$((CHOICE-1))]}"
|
||||
BUNDLE_NAME=$(basename "$BUNDLE_PATH")
|
||||
echo ""
|
||||
echo "Using: $BUNDLE_NAME"
|
||||
|
||||
# ── Verify the bundle ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Verifying bundle..."
|
||||
if ! git bundle verify "$BUNDLE_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Bundle verification failed." >&2
|
||||
echo "This usually means the bundle is incremental and your repo is missing" >&2
|
||||
echo "the prerequisite commits. Make sure you have applied earlier bundles first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── List refs in bundle ────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Branches in bundle:"
|
||||
BUNDLE_REFS=()
|
||||
while IFS= read -r line; do
|
||||
ref=$(echo "$line" | awk '{print $2}')
|
||||
BUNDLE_REFS+=("$ref")
|
||||
branch="${ref#refs/heads/}"
|
||||
echo " $branch"
|
||||
done < <(git bundle list-heads "$BUNDLE_PATH" | grep 'refs/heads/')
|
||||
|
||||
if [ ${#BUNDLE_REFS[@]} -eq 0 ]; then
|
||||
echo "No branch refs found in bundle." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Fetch each branch into refs/sync/incoming/<branch> ────────────────────────
|
||||
echo ""
|
||||
echo "Fetching..."
|
||||
INCOMING_BRANCHES=()
|
||||
for ref in "${BUNDLE_REFS[@]}"; do
|
||||
branch="${ref#refs/heads/}"
|
||||
incoming_ref="refs/sync/incoming/${branch}"
|
||||
git fetch "$BUNDLE_PATH" "${ref}:${incoming_ref}"
|
||||
INCOMING_BRANCHES+=("$branch")
|
||||
echo " Fetched: $branch -> $incoming_ref"
|
||||
done
|
||||
|
||||
# ── Show what's new per branch ────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── New commits ──────────────────────────────────────────────────────────"
|
||||
for branch in "${INCOMING_BRANCHES[@]}"; do
|
||||
incoming_ref="refs/sync/incoming/${branch}"
|
||||
if git rev-parse --verify "$branch" &>/dev/null; then
|
||||
# Branch exists locally
|
||||
ahead=$(git log --oneline "${branch}..${incoming_ref}" | wc -l)
|
||||
behind=$(git log --oneline "${incoming_ref}..${branch}" | wc -l)
|
||||
if [ "$ahead" -eq 0 ]; then
|
||||
echo " $branch: already up to date"
|
||||
continue
|
||||
fi
|
||||
echo " $branch: $ahead new commit(s) incoming, $behind local-only commit(s)"
|
||||
git log --oneline "${branch}..${incoming_ref}" | sed 's/^/ + /'
|
||||
if [ "$behind" -gt 0 ]; then
|
||||
echo " (your local commits not in bundle:)"
|
||||
git log --oneline "${incoming_ref}..${branch}" | sed 's/^/ * /'
|
||||
fi
|
||||
else
|
||||
n=$(git log --oneline "${incoming_ref}" | wc -l)
|
||||
echo " $branch: new branch with $n commit(s)"
|
||||
git log --oneline "${incoming_ref}" | head -10 | sed 's/^/ + /'
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ── Integrate each branch ─────────────────────────────────────────────────────
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
|
||||
for branch in "${INCOMING_BRANCHES[@]}"; do
|
||||
incoming_ref="refs/sync/incoming/${branch}"
|
||||
|
||||
if ! git rev-parse --verify "$branch" &>/dev/null; then
|
||||
# New branch — just create it
|
||||
read -rp "Create new local branch '$branch' from bundle? [Y/n] " ans
|
||||
ans="${ans:-Y}"
|
||||
if [[ "$ans" =~ ^[Yy] ]]; then
|
||||
git branch "$branch" "$incoming_ref"
|
||||
echo " Created branch '$branch'"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
ahead=$(git log --oneline "${branch}..${incoming_ref}" | wc -l)
|
||||
if [ "$ahead" -eq 0 ]; then
|
||||
continue # Nothing to do
|
||||
fi
|
||||
|
||||
behind=$(git log --oneline "${incoming_ref}..${branch}" | wc -l)
|
||||
|
||||
echo "── Integrating: $branch ─────────────────────────────────────────────"
|
||||
|
||||
if [ "$behind" -eq 0 ]; then
|
||||
# Fast-forward possible
|
||||
echo " Fast-forward possible."
|
||||
read -rp " Fast-forward '$branch'? [Y/n] " ans
|
||||
ans="${ans:-Y}"
|
||||
if [[ "$ans" =~ ^[Yy] ]]; then
|
||||
# Switch to branch if needed
|
||||
if [ "$CURRENT_BRANCH" != "$branch" ]; then
|
||||
git checkout "$branch"
|
||||
CURRENT_BRANCH="$branch"
|
||||
fi
|
||||
git merge --ff-only "$incoming_ref"
|
||||
echo " Done."
|
||||
fi
|
||||
else
|
||||
# Diverged — offer merge or rebase
|
||||
echo " Branches have diverged (you have $behind local commit(s))."
|
||||
echo " Options:"
|
||||
echo " [m] merge — creates a merge commit"
|
||||
echo " [r] rebase — replays your commits on top of incoming"
|
||||
echo " [s] skip — leave for manual handling"
|
||||
read -rp " Choice [m/r/s, default s]: " ans
|
||||
ans="${ans:-s}"
|
||||
case "$ans" in
|
||||
m|M)
|
||||
if [ "$CURRENT_BRANCH" != "$branch" ]; then
|
||||
git checkout "$branch"
|
||||
CURRENT_BRANCH="$branch"
|
||||
fi
|
||||
git merge "$incoming_ref" --no-edit
|
||||
echo " Merged."
|
||||
;;
|
||||
r|R)
|
||||
if [ "$CURRENT_BRANCH" != "$branch" ]; then
|
||||
git checkout "$branch"
|
||||
CURRENT_BRANCH="$branch"
|
||||
fi
|
||||
git rebase "$incoming_ref"
|
||||
echo " Rebased."
|
||||
;;
|
||||
*)
|
||||
echo " Skipped. Run manually:"
|
||||
echo " git checkout $branch && git merge refs/sync/incoming/$branch"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Import complete."
|
||||
echo ""
|
||||
echo "Tip: The fetched commits remain in refs/sync/incoming/<branch> until"
|
||||
echo "the next sync-pull run. You can inspect them anytime with:"
|
||||
echo " git log refs/sync/incoming/<branch>"
|
||||
121
sync-push.sh
Executable file
121
sync-push.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-push.sh — Export new commits as a git bundle to the shared folder.
|
||||
#
|
||||
# Usage: ./sync-push.sh [<share-dir>]
|
||||
# Default share dir: $SYNC_SHARE or ~/fifiletrans
|
||||
#
|
||||
# On first run: creates a full bundle of all branches.
|
||||
# On subsequent runs: creates an incremental bundle (only new commits).
|
||||
# Checkpoint per branch is stored in git config: sync.exported.<branch>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Config ────────────────────────────────────────────────────────────────────
|
||||
SHARE_DIR="${1:-${SYNC_SHARE:-$HOME/fifiletrans}}"
|
||||
|
||||
# ── Sanity checks ─────────────────────────────────────────────────────────────
|
||||
if ! git rev-parse --git-dir &>/dev/null; then
|
||||
echo "ERROR: Not inside a git repository." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
REPO_NAME=$(basename "$REPO_ROOT")
|
||||
HOST=$(hostname -s)
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BUNDLE_FILE="${REPO_NAME}-from-${HOST}-${TIMESTAMP}.bundle"
|
||||
BUNDLE_PATH="${SHARE_DIR}/${BUNDLE_FILE}"
|
||||
|
||||
mkdir -p "$SHARE_DIR"
|
||||
|
||||
# ── Collect branches ───────────────────────────────────────────────────────────
|
||||
BRANCHES=()
|
||||
while IFS= read -r branch; do
|
||||
BRANCHES+=("$branch")
|
||||
done < <(git branch --format='%(refname:short)')
|
||||
|
||||
if [ ${#BRANCHES[@]} -eq 0 ]; then
|
||||
echo "ERROR: No local branches found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Check if there is anything new to export ───────────────────────────────────
|
||||
has_new=false
|
||||
for branch in "${BRANCHES[@]}"; do
|
||||
checkpoint=$(git config "sync.exported.${branch}" 2>/dev/null || true)
|
||||
branch_tip=$(git rev-parse "$branch")
|
||||
if [ -z "$checkpoint" ] || ! git rev-parse --verify "$checkpoint" &>/dev/null || [ "$branch_tip" != "$checkpoint" ]; then
|
||||
has_new=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $has_new; then
|
||||
echo "Nothing new to export — all branches match the last export checkpoint."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Build rev-list args ────────────────────────────────────────────────────────
|
||||
# Include all branches; exclude commits already exported (per branch checkpoint)
|
||||
EXCLUSIONS=()
|
||||
for branch in "${BRANCHES[@]}"; do
|
||||
checkpoint=$(git config "sync.exported.${branch}" 2>/dev/null || true)
|
||||
if [ -n "$checkpoint" ] && git rev-parse --verify "$checkpoint" &>/dev/null; then
|
||||
EXCLUSIONS+=("^${checkpoint}")
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Report ─────────────────────────────────────────────────────────────────────
|
||||
echo "Repository : $REPO_NAME"
|
||||
echo "Host : $HOST"
|
||||
echo "Branches : ${BRANCHES[*]}"
|
||||
|
||||
if [ ${#EXCLUSIONS[@]} -eq 0 ]; then
|
||||
echo "Mode : full (first export)"
|
||||
NEW_COUNT=$(git log --oneline --all | wc -l)
|
||||
else
|
||||
echo "Mode : incremental"
|
||||
NEW_COUNT=$(git log --oneline "${BRANCHES[@]}" "${EXCLUSIONS[@]}" | wc -l)
|
||||
fi
|
||||
echo "New commits: $NEW_COUNT"
|
||||
|
||||
# ── Show what will be exported ─────────────────────────────────────────────────
|
||||
for branch in "${BRANCHES[@]}"; do
|
||||
checkpoint=$(git config "sync.exported.${branch}" 2>/dev/null || true)
|
||||
if [ -z "$checkpoint" ] || ! git rev-parse --verify "$checkpoint" &>/dev/null; then
|
||||
n=$(git log --oneline "$branch" | wc -l)
|
||||
else
|
||||
n=$(git log --oneline "${checkpoint}..${branch}" | wc -l)
|
||||
fi
|
||||
if [ "$n" -gt 0 ]; then
|
||||
echo " $branch: $n new commit(s)"
|
||||
git log --oneline "${checkpoint:-}${checkpoint:+..}${branch}" | sed 's/^/ /'
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ── Create bundle ──────────────────────────────────────────────────────────────
|
||||
git bundle create "$BUNDLE_PATH" "${BRANCHES[@]}" "${EXCLUSIONS[@]}"
|
||||
|
||||
# ── Update checkpoints ─────────────────────────────────────────────────────────
|
||||
for branch in "${BRANCHES[@]}"; do
|
||||
tip=$(git rev-parse "$branch")
|
||||
git config "sync.exported.${branch}" "$tip"
|
||||
done
|
||||
|
||||
# ── Done ───────────────────────────────────────────────────────────────────────
|
||||
SIZE=$(du -sh "$BUNDLE_PATH" | cut -f1)
|
||||
echo "Bundle created : $BUNDLE_PATH ($SIZE)"
|
||||
|
||||
# ── Optional Gitea auto-push ───────────────────────────────────────────────────
|
||||
if [ "$(git config sync.gitea.autopush 2>/dev/null)" = "true" ]; then
|
||||
GITEA_REMOTE=$(git config sync.gitea.remote 2>/dev/null || echo "gitea")
|
||||
echo ""
|
||||
echo "Auto-pushing to Gitea remote '$GITEA_REMOTE'..."
|
||||
git push "$GITEA_REMOTE" --all --tags
|
||||
echo "Gitea push done."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Next step on the other machine:"
|
||||
echo " ./sync-pull.sh $SHARE_DIR"
|
||||
BIN
tea-0.12.0-linux-arm64
Executable file
BIN
tea-0.12.0-linux-arm64
Executable file
Binary file not shown.
Reference in New Issue
Block a user