Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.0] - 2026-04-18

### Added

- **`git issue provider-migrate`** — Migrate Provider-IDs after repository renames, org transfers, or cross-platform moves
- Appends new Provider-ID commits to matching issues (preserves immutability, append-only)
- Supports dry-run mode (`--dry-run`) to preview changes
- Idempotent: safe to run multiple times without creating duplicate commits
- Works across all providers (GitHub, GitLab, Gitea, Forgejo)
- **Rename detection in `git issue sync`** — Warns when local issues have Provider-IDs from a different repo under the same owner, and suggests `provider-migrate` to prevent duplicates

## [1.3.3] - 2026-02-16

### Fixed
Expand Down Expand Up @@ -328,6 +339,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimized for large repositories
- Efficient Git plumbing usage

[1.4.0]: https://github.com/remenoscodes/git-native-issue/compare/v1.3.3...v1.4.0
[1.3.3]: https://github.com/remenoscodes/git-native-issue/compare/v1.3.2...v1.3.3
[1.3.2]: https://github.com/remenoscodes/git-native-issue/compare/v1.3.1...v1.3.2
[1.3.1]: https://github.com/remenoscodes/git-native-issue/compare/v1.3.0...v1.3.1
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Distributed issue tracking using Git's native data model. Issues stored as commi
Inherits workspace conventions from `~/CLAUDE.md`.

## Status
- **Version**: 1.3.3
- **Version**: 1.4.0
- **State**: active
- **Deploy**: Homebrew (`remenoscodes/git-native-issue/git-native-issue`), install script, Makefile

Expand All @@ -15,7 +15,7 @@ Platform bridges: `gh` (GitHub), `glab` (GitLab), REST API (Gitea/Forgejo).

## Key Commands
```bash
make test # Run all 282 tests
make test # Run all 296 tests
make install # Install system-wide (/usr/local)
git issue create "title" -l bug # Create issue with label
git issue ls --format full # List issues with metadata
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ test:
@sh t/test-merge.sh
@sh t/test-qol.sh
@sh t/test-comment-sync.sh
@sh t/test-provider-migrate.sh

clean:
rm -rf t/tmp-*
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![CI](https://github.com/remenoscodes/git-native-issue/actions/workflows/ci.yml/badge.svg)](https://github.com/remenoscodes/git-native-issue/actions/workflows/ci.yml)
[![License: GPL-2.0](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](https://www.gnu.org/licenses/gpl-2.0)
[![Version](https://img.shields.io/badge/version-1.3.3-green.svg)](https://github.com/remenoscodes/git-native-issue/releases)
[![Version](https://img.shields.io/badge/version-1.4.0-green.svg)](https://github.com/remenoscodes/git-native-issue/releases)

Distributed issue tracking using Git's native data model.

Expand Down Expand Up @@ -164,7 +164,7 @@ make install prefix=~ # User install (~/bin)

```bash
git issue version
# git-issue version 1.3.3
# git-issue version 1.4.0
```

## Commands
Expand All @@ -180,6 +180,7 @@ git issue version
| `git issue import` | Import issues from a platform (GitHub, GitLab, Gitea, Forgejo) |
| `git issue export` | Export issues to a platform |
| `git issue sync` | Two-way sync (import + export) |
| `git issue provider-migrate` | Migrate Provider-IDs after repo rename or transfer |
| `git issue search <pattern>` | Search issues by text |
| `git issue merge <remote>` | Merge issues from a remote |
| `git issue fsck` | Validate issue data integrity |
Expand Down Expand Up @@ -652,7 +653,7 @@ the deliverable that makes ecosystem adoption possible.
make test
```

282 tests: core (77), bridge (37), merge/fsck (21), QoL (22), validation (36), quality (59), edge cases (13), comment sync (8), concurrency (9).
296 tests: core (77), bridge (37), merge/fsck (21), QoL (22), validation (36), quality (59), edge cases (13), comment sync (8), concurrency (9), provider-migrate (14).

## Performance Notes

Expand Down
5 changes: 3 additions & 2 deletions bin/git-issue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

set -e

VERSION="1.3.3"
VERSION="1.4.0"

usage() {
cat <<EOF
Expand All @@ -25,6 +25,7 @@ Commands:
sync Two-way sync (import + export)
search Search issues by text pattern
merge Merge issues from a remote
provider-migrate Migrate Provider-IDs after repo renames or transfers
fsck Validate issue data integrity
init Configure the current repo for issue tracking
version Print version information
Expand All @@ -38,7 +39,7 @@ EOF
ISSUE_BIN_DIR="$(cd "$(dirname "$0")" && pwd)"

case "${1:-}" in
create|ls|show|comment|edit|state|search|import|export|sync|merge|fsck|init)
create|ls|show|comment|edit|state|search|import|export|sync|merge|fsck|init|provider-migrate)
cmd="$1"
shift
exec "$ISSUE_BIN_DIR/git-issue-$cmd" "$@"
Expand Down
178 changes: 178 additions & 0 deletions bin/git-issue-provider-migrate
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/bin/sh
#
# git-issue-provider-migrate - Migrate Provider-IDs after repo renames or transfers
#

set -e

. "$(dirname "$0")/git-issue-lib"

usage() {
cat <<EOF
usage: git issue provider-migrate [--dry-run] <old-prefix> <new-prefix>

Migrates Provider-IDs after repository renames, org transfers, or
cross-platform migrations. Appends new Provider-ID commits without
modifying existing history.

Options:
--dry-run Show what would be migrated without making changes
-h, --help Show this help

Examples:
git issue provider-migrate github:owner/old-repo github:owner/new-repo
git issue provider-migrate github:old-org/repo github:new-org/repo
git issue provider-migrate github:owner/repo gitlab:owner/repo
EOF
exit 1
}

dry_run=0
old_prefix=""
new_prefix=""

while test $# -gt 0
do
case "$1" in
--dry-run)
dry_run=1
shift
;;
-h|--help)
usage
;;
--)
shift
break
;;
-*)
echo "error: unknown option '$1'" >&2
usage
;;
*)
if test -z "$old_prefix"
then
old_prefix="$1"
elif test -z "$new_prefix"
then
new_prefix="$1"
else
echo "error: too many arguments" >&2
usage
fi
shift
;;
esac
done

if test -z "$old_prefix" || test -z "$new_prefix"
then
echo "error: both <old-prefix> and <new-prefix> are required" >&2
usage
fi

if test "$old_prefix" = "$new_prefix"
then
echo "error: old and new prefix are the same: $old_prefix" >&2
exit 1
fi

# Verify we are inside a git repository
git rev-parse --git-dir >/dev/null 2>&1 || {
echo "fatal: not a git repository" >&2
exit 128
}

# Validate git identity before creating commits (unless dry-run)
if test "$dry_run" -eq 0
then
validate_git_identity
fi

empty_tree="$(git hash-object -t tree /dev/null)"
migrated=0

for ref in $(git for-each-ref --format='%(refname)' refs/issues/)
do
uuid="${ref#refs/issues/}"
short_id="$(printf '%s' "$uuid" | cut -c1-7)"

# Get all Provider-IDs in the chain
all_pids="$(git log --format='%(trailers:key=Provider-ID,valueonly)' "$ref" | sed '/^$/d' | sed 's/^[[:space:]]*//')"

# Check if any Provider-ID matches the old prefix
matched_number=""
old_ifs="$IFS"
IFS='
'
for pid in $all_pids
do
case "$pid" in
"${old_prefix}#"*)
matched_number="${pid#*#}"
break
;;
esac
done
IFS="$old_ifs"

# Skip issues without a matching Provider-ID
test -z "$matched_number" && continue

new_pid="${new_prefix}#${matched_number}"

# Idempotency: skip if new Provider-ID already exists in chain
already_migrated=0
old_ifs="$IFS"
IFS='
'
for pid in $all_pids
do
if test "$pid" = "$new_pid"
then
already_migrated=1
break
fi
done
IFS="$old_ifs"

if test "$already_migrated" -eq 1
then
continue
fi

if test "$dry_run" -eq 1
then
printf '[dry-run] Would migrate %s: %s#%s -> %s\n' "$short_id" "$old_prefix" "$matched_number" "$new_pid"
else
# Get current HEAD of the issue
issue_head="$(git rev-parse "$ref")"

# Build commit message
tmpfile="$(mktemp)"
# Use a simple arrow since the Unicode arrow may cause issues in POSIX
printf 'Provider migrate: %s -> %s\n' "$old_prefix" "$new_prefix" > "$tmpfile"
git interpret-trailers --in-place --trailer "Provider-ID: $new_pid" "$tmpfile"

# Create new commit as child of current HEAD
commit="$(git commit-tree -p "$issue_head" -- "$empty_tree" < "$tmpfile")"
rm -f "$tmpfile"

# Update the ref
git update-ref "$ref" "$commit" "$issue_head"

printf 'Migrated %s: %s#%s -> %s\n' "$short_id" "$old_prefix" "$matched_number" "$new_pid"
fi

migrated=$((migrated + 1))
done

_verb="Migrated"
test "$dry_run" -eq 1 && _verb="Would migrate"

if test "$migrated" -eq 1
then
printf '%s %d issue\n' "$_verb" "$migrated"
else
printf '%s %d issues\n' "$_verb" "$migrated"
fi
62 changes: 62 additions & 0 deletions bin/git-issue-sync
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,67 @@ ISSUE_BIN_DIR="$(cd "$(dirname "$0")" && pwd)"
printf '=== Importing from %s ===\n' "$provider"
"$ISSUE_BIN_DIR/git-issue-import" "$provider" --state "$state_filter" $dry_run

# --- Rename detection: warn if issues have Provider-IDs from a different repo name ---
_sync_type="${provider%%:*}"
_sync_path="${provider#*:}"
_sync_owner="${_sync_path%/*}"
_sync_repo="${_sync_path#*/}"
_sync_owner_prefix="${_sync_type}:${_sync_owner}/"

_mismatch_file="$(mktemp)"
: > "$_mismatch_file"

for _ref in $(git for-each-ref --format='%(refname)' refs/issues/)
do
git log --format='%(trailers:key=Provider-ID,valueonly)' "$_ref" | \
sed '/^$/d' | sed 's/^[[:space:]]*//' | while read -r _pid
do
# Does this PID belong to the same owner/type but different repo?
case "$_pid" in
"${_sync_owner_prefix}"*)
_pid_repo="${_pid#"${_sync_owner_prefix}"}"
_pid_repo="${_pid_repo%%#*}"
if test "$_pid_repo" != "$_sync_repo"
then
# Check if this issue already has a PID for the target repo
_has_new_pid=""
git log --format='%(trailers:key=Provider-ID,valueonly)' "$_ref" | \
sed '/^$/d' | sed 's/^[[:space:]]*//' | while read -r _check_pid
do
case "$_check_pid" in
"${_sync_owner_prefix}${_sync_repo}#"*)
printf 'yes' > "$_mismatch_file.found"
;;
esac
done
if test -f "$_mismatch_file.found"
then
rm -f "$_mismatch_file.found"
else
printf '%s\n' "${_sync_owner_prefix}${_pid_repo}" >> "$_mismatch_file"
fi
fi
;;
esac
done
done

if test -s "$_mismatch_file"
then
printf '\n'
printf 'Warning: found issues with Provider-IDs from a different repo name:\n'
sort "$_mismatch_file" | uniq -c | while read -r _count _old_prefix
do
printf ' %s (%d issues)\n' "$_old_prefix" "$_count"
done
printf 'If the repo was renamed or transferred, run:\n'
# Show the first mismatched prefix as example
_first_old="$(sort "$_mismatch_file" | head -1)"
printf ' git issue provider-migrate %s %s\n' "$_first_old" "${_sync_owner_prefix}${_sync_repo}"
printf '\n'
fi
rm -f "$_mismatch_file" "$_mismatch_file.found"
# --- End rename detection ---

printf '\n=== Exporting to %s ===\n' "$provider"
"$ISSUE_BIN_DIR/git-issue-export" "$provider" $dry_run
Loading
Loading