[GH-ISSUE #173] [1.0.11 Degradation]: AI attribution incorrectly applied to human code after rebase conflict resolution #64

Closed
opened 2026-03-02 04:11:35 +03:00 by kerem · 2 comments
Owner

Originally created by @AtnesNess on GitHub (Oct 31, 2025).
Original GitHub issue: https://github.com/git-ai-project/git-ai/issues/173

When resolving merge conflicts during a git rebase operation, git-ai incorrectly attributes human-written code to the AI agent. Specifically, code that was written by a human developer on the target branch is being misattributed to the AI after the rebase completes.

Steps to Reproduce

  1. Create a file with initial content and commit it
  2. Create a feature branch where AI modifies the file (using git-ai checkpoint mock_ai)
  3. Switch back to main branch and have a human make conflicting changes to the same file
  4. Attempt to rebase the feature branch onto main (will create a conflict)
  5. Resolve the conflict by keeping both AI and human changes
  6. Continue the rebase with git rebase --continue
  7. Check attribution with git-ai blame

Expected Behavior

After conflict resolution:

  • AI-authored code (6 lines from function_two enhancement and ai_function) should be attributed to mock_ai
  • Human-authored code (2 lines from human_function) should be attributed to Test User
  • Stats should show: "ai_additions": 6

Actual Behavior

After conflict resolution:

  • AI-authored code (6 lines) is correctly attributed to mock_ai
  • Human-authored code (2 lines from human_function) is INCORRECTLY attributed to mock_ai
  • Stats incorrectly show: "ai_additions": 8 (includes the 2 human lines)

Evidence from git-ai blame output

62d29b0 (Test User 2025-10-31 17:33:04 +0000  1) def function_one():
62d29b0 (Test User 2025-10-31 17:33:04 +0000  2)     return 1
62d29b0 (Test User 2025-10-31 17:33:04 +0000  3) def function_two():
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  4)     # AI enhanced this function
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  5)     result = 2 * 2
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  6)     return result
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  7) def ai_function():
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  8)     print("AI added this")
50cb6df (mock_ai   2025-10-31 17:33:04 +0000  9)     return "ai_data"
8aa16d3 (mock_ai   2025-10-31 17:33:04 +0000 10) def human_function():  ❌ WRONG
8aa16d3 (mock_ai   2025-10-31 17:33:04 +0000 11)     return "human_data" ❌ WRONG

Lines 10-11 should be attributed to Test User, not mock_ai.

Stats Comparison

Expected Stats

{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 6,
  "ai_accepted": 6,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 3,
  "git_diff_added_lines": 6,
  "human_deletions": 2,
  "ai_deletions": 1,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 6,
      "mixed_additions": 0,
      "ai_accepted": 6,
      "ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}

Actual Stats

{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 8,
  "ai_accepted": 8,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 3,
  "git_diff_added_lines": 6,
  "human_deletions": 2,
  "ai_deletions": 1,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 8,
      "mixed_additions": 0,
      "ai_accepted": 8,
      "ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}

Difference: ai_additions is 8 instead of 6 (2 extra lines incorrectly attributed)

Critical Logical Inconsistency

The stats show ai_additions: 8 but git_diff_added_lines: 6 - this is logically impossible!

The AI cannot have added 8 lines when Git diff only shows 6 total lines were added in the commit. The ai_additions value should never exceed git_diff_added_lines, as AI additions must be a subset of all additions. This reveals that the attribution system is fundamentally broken during rebase conflict resolution, not just miscounting but producing mathematically impossible results.

Script to reproduce

> git-ai -v
1.0.11
#!/bin/bash
set -e

# Reproduction script for:
# "AI attribution is preserved after fixing conflict during rebase"
# 
# This script validates that git-ai preserves AI authorship information
# when resolving merge conflicts during a rebase operation.
#
# Expected behavior: AI attribution should be preserved after conflict resolution
# Actual behavior: AI attribution may be lost or incorrectly attributed

# Returns: 0 if JSONs match, 1 otherwise
compare_json() {
    local expected_json="$1"
    local actual_json="$2"
    local error_prefix="${3:-JSON mismatch}"
    
    # Check if jq is available
    if ! command -v jq &> /dev/null; then
        echo "WARNING: jq not available, skipping JSON comparison" 
        return 0
    fi
    
    # Verify both inputs are valid JSON
    if ! echo "$expected_json" | jq . >/dev/null 2>&1; then
        echo "ERROR: Expected JSON is invalid" 
        return 1
    fi
    
    if ! echo "$actual_json" | jq . >/dev/null 2>&1; then
        echo "ERROR: Actual JSON is invalid" 
        echo "Actual: $actual_json" 
        return 1
    fi
    
    # Canonicalize both JSONs (sort keys, compact format)
    local expected_canonical=$(echo "$expected_json" | jq -cS .)
    local actual_canonical=$(echo "$actual_json" | jq -cS .)
    
    # Compare canonicalized JSONs
    if [ "$expected_canonical" != "$actual_canonical" ]; then
        echo "ERROR: $error_prefix" 
        echo "" 
        echo "Expected (formatted):" 
        echo "$expected_json" | jq . 
        echo "" 
        echo "Actual (formatted):" 
        echo "$actual_json" | jq . 
        echo "" 
        echo "Expected (canonical): $expected_canonical" 
        echo "Actual (canonical):   $actual_canonical" 
        exit 1
    fi
}


echo "=========================================="
echo "Reproducing: AI Attribution Lost During Rebase Conflict Resolution"
echo "=========================================="
echo ""

# Check if git-ai is available
if ! command -v git-ai &> /dev/null; then
    echo "ERROR: git-ai command not found. Please install git-ai first."
    exit 1
fi

# Create temporary directory for test
TEST_DIR=$(mktemp -d)
echo "Test directory: $TEST_DIR"
cd "$TEST_DIR"

# Initialize git repo
git init
git config user.email "test@example.com"
git config user.name "Test User"

# Create initial commit (required for git-ai)
echo "# Test Project" > README.md
git add README.md
git commit -m "Initial commit"


 # Step 1: Create initial file on main branch
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    return 2
EOF
    git-ai checkpoint
    git add shared.py
    git commit -m "Initial shared file"
    
    # Step 2: Create feature branch where AI modifies the file
    git checkout -b feature-ai
    
    # AI modifies function_two and adds new content
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # AI enhanced this function
    result = 2 * 2
    return result
def ai_function():
    print("AI added this")
    return "ai_data"
EOF
    
    git-ai checkpoint mock_ai shared.py
    git add shared.py
    git commit -m "AI enhances function_two and adds ai_function"
    
    # Get stats before conflict resolution
    ai_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq)
    echo "=== AI Stats BEFORE conflict resolution ==="
    
    # Verify AI authorship before conflict
    git-ai blame shared.py | cat

    echo "$ai_stats_before" 
    
    blame_before=$(git-ai blame shared.py)
    [[ "$blame_before" =~ "mock_ai" ]] || {
        echo "ERROR: 'mock_ai' not found in blame output before conflict" 
        return 1
    }

    expected_json='{
        "human_additions": 0,
        "mixed_additions": 0,
        "ai_additions": 6,
        "ai_accepted": 6,
        "time_waiting_for_ai": 0,
        "git_diff_deleted_lines": 1,
        "git_diff_added_lines": 6,
        "human_deletions": 0,
        "ai_deletions": 1,
        "tool_model_breakdown": {
            "mock_ai::unknown": {
            "ai_additions": 6,
            "mixed_additions": 0,
            "ai_accepted": 6,
            "ai_deletions": 1,
            "time_waiting_for_ai": 0
            }
        }
    }'

    compare_json "$expected_json" "$ai_stats_before" "AI Stats before conflict resolution do not match expected" || return 1
    
    # Step 3: Go back to main and make conflicting changes
    git checkout main
    
    # Human modifies function_two differently (will cause conflict)
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # Human modified this differently
    value = 2 + 2
    return value
def human_function():
    return "human_data"
EOF
    
    git-ai checkpoint
    git add shared.py
    git commit -m "Human modifies function_two and adds human_function"

    echo "=== Human Stats BEFORE conflict resolution ===" 
    git-ai blame shared.py | cat
    human_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq)
    echo "$human_stats_before" 

    expected_json='{
        "human_additions": 5,
        "mixed_additions": 0,
        "ai_additions": 0,
        "ai_accepted": 0,
        "time_waiting_for_ai": 0,
        "git_diff_deleted_lines": 1,
        "git_diff_added_lines": 5,
        "human_deletions": 1,
        "ai_deletions": 0,
        "tool_model_breakdown": {}
    }'

    compare_json "$expected_json" "$human_stats_before" "Human Stats before conflict do not match expected" || return 1
    
    # Step 4: Attempt rebase - this will cause a conflict
    git checkout feature-ai
    echo "=== Attempting rebase (will conflict) ===" 
    
    # Rebase will stop due to conflict
    if git rebase main 2>&1; then
        echo "ERROR: Expected rebase to fail with conflict, but it succeeded" 
        return 1
    fi
    
    # Verify we're in a conflicted state
    git status 
    
    # Step 5: Resolve the conflict by keeping both changes
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # AI enhanced this function
    result = 2 * 2
    return result
def ai_function():
    print("AI added this")
    return "ai_data"
def human_function():
    return "human_data"
EOF
    
    # Mark conflict as resolved
    git add shared.py
    
    # Continue rebase (set GIT_EDITOR to bypass interactive editor)
    echo "=== Continuing rebase after conflict resolution ===" 
    GIT_EDITOR=true git rebase --continue
    
    # Step 6: Verify AI authorship is preserved after conflict resolution
    echo "=== Stats AFTER conflict resolution ===" 
    git-ai blame shared.py | cat
    feature_commit_after=$(git rev-parse HEAD)
    stats_after=$(git-ai stats --json "$feature_commit_after" | jq)
    echo "$stats_after" 
    
    
    # The stats should show AI additions preserved after conflict resolution
    # AI added 6 lines (function_two enhancement: 3 lines, ai_function: 3 lines)
    # Note: The conflict resolution shows deletions from both human and AI sides
    expected_json='{
      "human_additions": 0,
      "mixed_additions": 0,
      "ai_additions": 6,
      "ai_accepted": 6,
      "time_waiting_for_ai": 0,
      "git_diff_deleted_lines": 3,
      "git_diff_added_lines": 6,
      "human_deletions": 2,
      "ai_deletions": 1,
      "tool_model_breakdown": {
        "mock_ai::unknown": {
          "ai_additions": 6,
          "mixed_additions": 0,
          "ai_accepted": 6,
          "ai_deletions": 1,
          "time_waiting_for_ai": 0
        }
      }
    }'
    
    compare_json "$expected_json" "$stats_after" "Stats after conflict resolution do not match expected" || return 1
    
    # Verify blame shows AI authorship for AI lines
    echo "=== Blame AFTER conflict resolution ===" 
    git-ai blame shared.py | cat
    # Show the diff for informational purposes
    echo "=== Git Diff after conflict resolution ===" 
    git diff HEAD^ HEAD -- shared.py 
    
    blame_after=$(git-ai blame shared.py)
    
    # Verify AI attribution is present
    [[ "$blame_after" =~ "mock_ai" ]] || {
        echo "ERROR: 'mock_ai' not found in blame output after conflict resolution" 
        echo "AI authorship was NOT preserved after conflict resolution!" 
        return 1
    }
    
    # Verify human attribution is also present
    [[ "$blame_after" =~ "Test User" ]] || {
        echo "ERROR: 'Test User' not found in blame output after conflict resolution" 
        return 1
    }
    
    # Verify the file has expected content
    grep -q "AI added this" shared.py || {
        echo "ERROR: AI content not found in resolved file" 
        return 1
    }
    
    grep -q "human_data" shared.py || {
        echo "ERROR: Human content not found in resolved file" 
        return 1
    }
    
    echo "✓ AI attribution successfully preserved after conflict resolution during rebase" 
Originally created by @AtnesNess on GitHub (Oct 31, 2025). Original GitHub issue: https://github.com/git-ai-project/git-ai/issues/173 When resolving merge conflicts during a `git rebase` operation, `git-ai` incorrectly attributes human-written code to the AI agent. Specifically, code that was written by a human developer on the target branch is being misattributed to the AI after the rebase completes. ## Steps to Reproduce 1. Create a file with initial content and commit it 2. Create a feature branch where AI modifies the file (using `git-ai checkpoint mock_ai`) 3. Switch back to main branch and have a human make conflicting changes to the same file 4. Attempt to rebase the feature branch onto main (will create a conflict) 5. Resolve the conflict by keeping both AI and human changes 6. Continue the rebase with `git rebase --continue` 7. Check attribution with `git-ai blame` ## Expected Behavior After conflict resolution: - AI-authored code (6 lines from `function_two` enhancement and `ai_function`) should be attributed to `mock_ai` - Human-authored code (2 lines from `human_function`) should be attributed to `Test User` - Stats should show: `"ai_additions": 6` ## Actual Behavior After conflict resolution: - AI-authored code (6 lines) is correctly attributed to `mock_ai` - **Human-authored code (2 lines from `human_function`) is INCORRECTLY attributed to `mock_ai`** - Stats incorrectly show: `"ai_additions": 8` (includes the 2 human lines) ## Evidence from `git-ai blame` output ``` 62d29b0 (Test User 2025-10-31 17:33:04 +0000 1) def function_one(): 62d29b0 (Test User 2025-10-31 17:33:04 +0000 2) return 1 62d29b0 (Test User 2025-10-31 17:33:04 +0000 3) def function_two(): 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 4) # AI enhanced this function 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 5) result = 2 * 2 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 6) return result 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 7) def ai_function(): 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 8) print("AI added this") 50cb6df (mock_ai 2025-10-31 17:33:04 +0000 9) return "ai_data" 8aa16d3 (mock_ai 2025-10-31 17:33:04 +0000 10) def human_function(): ❌ WRONG 8aa16d3 (mock_ai 2025-10-31 17:33:04 +0000 11) return "human_data" ❌ WRONG ``` Lines 10-11 should be attributed to `Test User`, not `mock_ai`. ## Stats Comparison ### Expected Stats ```json { "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "human_deletions": 2, "ai_deletions": 1, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "ai_deletions": 1, "time_waiting_for_ai": 0 } } } ``` ### Actual Stats ```json { "human_additions": 0, "mixed_additions": 0, "ai_additions": 8, "ai_accepted": 8, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "human_deletions": 2, "ai_deletions": 1, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 8, "mixed_additions": 0, "ai_accepted": 8, "ai_deletions": 1, "time_waiting_for_ai": 0 } } } ``` **Difference:** `ai_additions` is 8 instead of 6 (2 extra lines incorrectly attributed) ### Critical Logical Inconsistency **The stats show `ai_additions: 8` but `git_diff_added_lines: 6`** - this is logically impossible! The AI cannot have added 8 lines when Git diff only shows 6 total lines were added in the commit. The `ai_additions` value should never exceed `git_diff_added_lines`, as AI additions must be a subset of all additions. This reveals that the attribution system is fundamentally broken during rebase conflict resolution, not just miscounting but producing mathematically impossible results. ### Script to reproduce ``` > git-ai -v 1.0.11 ``` ``` #!/bin/bash set -e # Reproduction script for: # "AI attribution is preserved after fixing conflict during rebase" # # This script validates that git-ai preserves AI authorship information # when resolving merge conflicts during a rebase operation. # # Expected behavior: AI attribution should be preserved after conflict resolution # Actual behavior: AI attribution may be lost or incorrectly attributed # Returns: 0 if JSONs match, 1 otherwise compare_json() { local expected_json="$1" local actual_json="$2" local error_prefix="${3:-JSON mismatch}" # Check if jq is available if ! command -v jq &> /dev/null; then echo "WARNING: jq not available, skipping JSON comparison" return 0 fi # Verify both inputs are valid JSON if ! echo "$expected_json" | jq . >/dev/null 2>&1; then echo "ERROR: Expected JSON is invalid" return 1 fi if ! echo "$actual_json" | jq . >/dev/null 2>&1; then echo "ERROR: Actual JSON is invalid" echo "Actual: $actual_json" return 1 fi # Canonicalize both JSONs (sort keys, compact format) local expected_canonical=$(echo "$expected_json" | jq -cS .) local actual_canonical=$(echo "$actual_json" | jq -cS .) # Compare canonicalized JSONs if [ "$expected_canonical" != "$actual_canonical" ]; then echo "ERROR: $error_prefix" echo "" echo "Expected (formatted):" echo "$expected_json" | jq . echo "" echo "Actual (formatted):" echo "$actual_json" | jq . echo "" echo "Expected (canonical): $expected_canonical" echo "Actual (canonical): $actual_canonical" exit 1 fi } echo "==========================================" echo "Reproducing: AI Attribution Lost During Rebase Conflict Resolution" echo "==========================================" echo "" # Check if git-ai is available if ! command -v git-ai &> /dev/null; then echo "ERROR: git-ai command not found. Please install git-ai first." exit 1 fi # Create temporary directory for test TEST_DIR=$(mktemp -d) echo "Test directory: $TEST_DIR" cd "$TEST_DIR" # Initialize git repo git init git config user.email "test@example.com" git config user.name "Test User" # Create initial commit (required for git-ai) echo "# Test Project" > README.md git add README.md git commit -m "Initial commit" # Step 1: Create initial file on main branch cat > shared.py <<EOF def function_one(): return 1 def function_two(): return 2 EOF git-ai checkpoint git add shared.py git commit -m "Initial shared file" # Step 2: Create feature branch where AI modifies the file git checkout -b feature-ai # AI modifies function_two and adds new content cat > shared.py <<EOF def function_one(): return 1 def function_two(): # AI enhanced this function result = 2 * 2 return result def ai_function(): print("AI added this") return "ai_data" EOF git-ai checkpoint mock_ai shared.py git add shared.py git commit -m "AI enhances function_two and adds ai_function" # Get stats before conflict resolution ai_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq) echo "=== AI Stats BEFORE conflict resolution ===" # Verify AI authorship before conflict git-ai blame shared.py | cat echo "$ai_stats_before" blame_before=$(git-ai blame shared.py) [[ "$blame_before" =~ "mock_ai" ]] || { echo "ERROR: 'mock_ai' not found in blame output before conflict" return 1 } expected_json='{ "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 6, "human_deletions": 0, "ai_deletions": 1, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "ai_deletions": 1, "time_waiting_for_ai": 0 } } }' compare_json "$expected_json" "$ai_stats_before" "AI Stats before conflict resolution do not match expected" || return 1 # Step 3: Go back to main and make conflicting changes git checkout main # Human modifies function_two differently (will cause conflict) cat > shared.py <<EOF def function_one(): return 1 def function_two(): # Human modified this differently value = 2 + 2 return value def human_function(): return "human_data" EOF git-ai checkpoint git add shared.py git commit -m "Human modifies function_two and adds human_function" echo "=== Human Stats BEFORE conflict resolution ===" git-ai blame shared.py | cat human_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq) echo "$human_stats_before" expected_json='{ "human_additions": 5, "mixed_additions": 0, "ai_additions": 0, "ai_accepted": 0, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 5, "human_deletions": 1, "ai_deletions": 0, "tool_model_breakdown": {} }' compare_json "$expected_json" "$human_stats_before" "Human Stats before conflict do not match expected" || return 1 # Step 4: Attempt rebase - this will cause a conflict git checkout feature-ai echo "=== Attempting rebase (will conflict) ===" # Rebase will stop due to conflict if git rebase main 2>&1; then echo "ERROR: Expected rebase to fail with conflict, but it succeeded" return 1 fi # Verify we're in a conflicted state git status # Step 5: Resolve the conflict by keeping both changes cat > shared.py <<EOF def function_one(): return 1 def function_two(): # AI enhanced this function result = 2 * 2 return result def ai_function(): print("AI added this") return "ai_data" def human_function(): return "human_data" EOF # Mark conflict as resolved git add shared.py # Continue rebase (set GIT_EDITOR to bypass interactive editor) echo "=== Continuing rebase after conflict resolution ===" GIT_EDITOR=true git rebase --continue # Step 6: Verify AI authorship is preserved after conflict resolution echo "=== Stats AFTER conflict resolution ===" git-ai blame shared.py | cat feature_commit_after=$(git rev-parse HEAD) stats_after=$(git-ai stats --json "$feature_commit_after" | jq) echo "$stats_after" # The stats should show AI additions preserved after conflict resolution # AI added 6 lines (function_two enhancement: 3 lines, ai_function: 3 lines) # Note: The conflict resolution shows deletions from both human and AI sides expected_json='{ "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "human_deletions": 2, "ai_deletions": 1, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "ai_deletions": 1, "time_waiting_for_ai": 0 } } }' compare_json "$expected_json" "$stats_after" "Stats after conflict resolution do not match expected" || return 1 # Verify blame shows AI authorship for AI lines echo "=== Blame AFTER conflict resolution ===" git-ai blame shared.py | cat # Show the diff for informational purposes echo "=== Git Diff after conflict resolution ===" git diff HEAD^ HEAD -- shared.py blame_after=$(git-ai blame shared.py) # Verify AI attribution is present [[ "$blame_after" =~ "mock_ai" ]] || { echo "ERROR: 'mock_ai' not found in blame output after conflict resolution" echo "AI authorship was NOT preserved after conflict resolution!" return 1 } # Verify human attribution is also present [[ "$blame_after" =~ "Test User" ]] || { echo "ERROR: 'Test User' not found in blame output after conflict resolution" return 1 } # Verify the file has expected content grep -q "AI added this" shared.py || { echo "ERROR: AI content not found in resolved file" return 1 } grep -q "human_data" shared.py || { echo "ERROR: Human content not found in resolved file" return 1 } echo "✓ AI attribution successfully preserved after conflict resolution during rebase" ```
kerem closed this issue 2026-03-02 04:11:35 +03:00
Author
Owner

@svarlamov commented on GitHub (Nov 1, 2025):

Thank you for the reproduction shell script -- extremely useful. Confirmed that this is fixed in 1.0.13

<!-- gh-comment-id:3475725892 --> @svarlamov commented on GitHub (Nov 1, 2025): Thank you for the reproduction shell script -- extremely useful. Confirmed that this is fixed in 1.0.13
Author
Owner

@AtnesNess commented on GitHub (Nov 3, 2025):

I still see an issue: Stats incorrectly show: "ai_additions": 8

➜ git-ai -v
1.0.15

LOG

bash test.sh
==========================================
Reproducing: AI Attribution Lost During Rebase Conflict Resolution
==========================================

Test directory: /tmp/tmp.gNTVVPXu9G
Initialized empty Git repository in /tmp/tmp.gNTVVPXu9G/.git/
[main (root-commit) 193758c] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 README.md
you  ████████████████████████████████████████ ai
     100%                                   0%
human Test User changed 1 file(s) that have changed since the last commit
Checkpoint completed in 27.180889ms
[main 68b89bb] Initial shared file
 1 file changed, 4 insertions(+)
 create mode 100644 shared.py
you  ████████████████████████████████████████ ai
     100%                                   0%
Switched to a new branch 'feature-ai'
ai_agent mock_ai changed 1 file(s) that have changed since the last commit
Checkpoint completed in 37.02687ms
[feature-ai 5cd7aa0] AI enhances function_two and adds ai_function
 1 file changed, 6 insertions(+), 1 deletion(-)
you  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ai
     0%                                  100%
     100% AI code accepted
[DEBUG] Stats command found commit: 5cd7aa0e9992243187cd0465202ca1b446478c26 refname: 5cd7aa0e9992243187cd0465202ca1b446478c26
=== AI Stats BEFORE conflict resolution ===
68b89bb (Test User 2025-11-03 20:31:23 +0000 1) def function_one():
68b89bb (Test User 2025-11-03 20:31:23 +0000 2)     return 1
68b89bb (Test User 2025-11-03 20:31:23 +0000 3) def function_two():
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 4)     # AI enhanced this function
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 5)     result = 2 * 2
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 6)     return result
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 7) def ai_function():
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 8)     print("AI added this")
5cd7aa0 (mock_ai   2025-11-03 20:31:23 +0000 9)     return "ai_data"
{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 6,
  "ai_accepted": 6,
  "total_ai_additions": 6,
  "total_ai_deletions": 1,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 1,
  "git_diff_added_lines": 6,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 6,
      "mixed_additions": 0,
      "ai_accepted": 6,
      "total_ai_additions": 6,
      "total_ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}
Switched to branch 'main'
human Test User changed 1 file(s) that have changed since the last commit
Checkpoint completed in 36.592686ms
[main 8b8b4cf] Human modifies function_two and adds human_function
 1 file changed, 5 insertions(+), 1 deletion(-)
you  ████████████████████████████████████████ ai
     100%                                   0%
=== Human Stats BEFORE conflict resolution ===
68b89bb (Test User 2025-11-03 20:31:23 +0000 1) def function_one():
68b89bb (Test User 2025-11-03 20:31:23 +0000 2)     return 1
68b89bb (Test User 2025-11-03 20:31:23 +0000 3) def function_two():
8b8b4cf (Test User 2025-11-03 20:31:23 +0000 4)     # Human modified this differently
8b8b4cf (Test User 2025-11-03 20:31:23 +0000 5)     value = 2 + 2
8b8b4cf (Test User 2025-11-03 20:31:23 +0000 6)     return value
8b8b4cf (Test User 2025-11-03 20:31:23 +0000 7) def human_function():
8b8b4cf (Test User 2025-11-03 20:31:23 +0000 8)     return "human_data"
[DEBUG] Stats command found commit: 8b8b4cf5aa195f298d5cd26d6e9a4b299a4a4232 refname: 8b8b4cf5aa195f298d5cd26d6e9a4b299a4a4232
{
  "human_additions": 5,
  "mixed_additions": 0,
  "ai_additions": 0,
  "ai_accepted": 0,
  "total_ai_additions": 0,
  "total_ai_deletions": 0,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 1,
  "git_diff_added_lines": 5,
  "tool_model_breakdown": {}
}
Switched to branch 'feature-ai'
=== Attempting rebase (will conflict) ===
Auto-merging shared.py
CONFLICT (content): Merge conflict in shared.py
error: could not apply 5cd7aa0... AI enhances function_two and adds ai_function
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply 5cd7aa0... # AI enhances function_two and adds ai_function
interactive rebase in progress; onto 8b8b4cf
Last command done (1 command done):
   pick 5cd7aa0 # AI enhances function_two and adds ai_function
No commands remaining.
You are currently rebasing branch 'feature-ai' on '8b8b4cf'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
        both modified:   shared.py

no changes added to commit (use "git add" and/or "git commit -a")
=== Continuing rebase after conflict resolution ===
[detached HEAD 7b8965e] AI enhances function_two and adds ai_function
 1 file changed, 6 insertions(+), 3 deletions(-)
Successfully rebased and updated refs/heads/feature-ai.
=== Stats AFTER conflict resolution ===
68b89bb (Test User 2025-11-03 20:31:23 +0000  1) def function_one():
68b89bb (Test User 2025-11-03 20:31:23 +0000  2)     return 1
68b89bb (Test User 2025-11-03 20:31:23 +0000  3) def function_two():
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  4)     # AI enhanced this function
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  5)     result = 2 * 2
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  6)     return result
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  7) def ai_function():
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  8)     print("AI added this")
7b8965e (mock_ai   2025-11-03 20:31:23 +0000  9)     return "ai_data"
8b8b4cf (mock_ai   2025-11-03 20:31:23 +0000 10) def human_function():
8b8b4cf (mock_ai   2025-11-03 20:31:23 +0000 11)     return "human_data"
[DEBUG] Stats command found commit: 7b8965e2c1ef0cfaca153e8c0f3d5385bc97922b refname: 7b8965e2c1ef0cfaca153e8c0f3d5385bc97922b
{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 6,
  "ai_accepted": 8,
  "total_ai_additions": 6,
  "total_ai_deletions": 1,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 3,
  "git_diff_added_lines": 6,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 6,
      "mixed_additions": 0,
      "ai_accepted": 8,
      "total_ai_additions": 6,
      "total_ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}
ERROR: Stats after conflict resolution do not match expected

Expected (formatted):
{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 6,
  "ai_accepted": 6,
  "total_ai_additions": 6,
  "total_ai_deletions": 1,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 3,
  "git_diff_added_lines": 6,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 6,
      "mixed_additions": 0,
      "ai_accepted": 6,
      "total_ai_additions": 6,
      "total_ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}

Actual (formatted):
{
  "human_additions": 0,
  "mixed_additions": 0,
  "ai_additions": 6,
  "ai_accepted": 8,
  "total_ai_additions": 6,
  "total_ai_deletions": 1,
  "time_waiting_for_ai": 0,
  "git_diff_deleted_lines": 3,
  "git_diff_added_lines": 6,
  "tool_model_breakdown": {
    "mock_ai::unknown": {
      "ai_additions": 6,
      "mixed_additions": 0,
      "ai_accepted": 8,
      "total_ai_additions": 6,
      "total_ai_deletions": 1,
      "time_waiting_for_ai": 0
    }
  }
}

Expected (canonical): {"ai_accepted":6,"ai_additions":6,"git_diff_added_lines":6,"git_diff_deleted_lines":3,"human_additions":0,"mixed_additions":0,"time_waiting_for_ai":0,"tool_model_breakdown":{"mock_ai::unknown":{"ai_accepted":6,"ai_additions":6,"mixed_additions":0,"time_waiting_for_ai":0,"total_ai_additions":6,"total_ai_deletions":1}},"total_ai_additions":6,"total_ai_deletions":1}
Actual (canonical):   {"ai_accepted":8,"ai_additions":6,"git_diff_added_lines":6,"git_diff_deleted_lines":3,"human_additions":0,"mixed_additions":0,"time_waiting_for_ai":0,"tool_model_breakdown":{"mock_ai::unknown":{"ai_accepted":8,"ai_additions":6,"mixed_additions":0,"time_waiting_for_ai":0,"total_ai_additions":6,"total_ai_deletions":1}},"total_ai_additions":6,"total_ai_deletions":1}

Script

#!/bin/bash
set -e

# Reproduction script for:
# "AI attribution is preserved after fixing conflict during rebase"
# 
# This script validates that git-ai preserves AI authorship information
# when resolving merge conflicts during a rebase operation.
#
# Expected behavior: AI attribution should be preserved after conflict resolution
# Actual behavior: AI attribution may be lost or incorrectly attributed

# Returns: 0 if JSONs match, 1 otherwise
compare_json() {
    local expected_json="$1"
    local actual_json="$2"
    local error_prefix="${3:-JSON mismatch}"
    
    # Check if jq is available
    if ! command -v jq &> /dev/null; then
        echo "WARNING: jq not available, skipping JSON comparison" 
        return 0
    fi
    
    # Verify both inputs are valid JSON
    if ! echo "$expected_json" | jq . >/dev/null 2>&1; then
        echo "ERROR: Expected JSON is invalid" 
        return 1
    fi
    
    if ! echo "$actual_json" | jq . >/dev/null 2>&1; then
        echo "ERROR: Actual JSON is invalid" 
        echo "Actual: $actual_json" 
        return 1
    fi
    
    # Canonicalize both JSONs (sort keys, compact format)
    local expected_canonical=$(echo "$expected_json" | jq -cS .)
    local actual_canonical=$(echo "$actual_json" | jq -cS .)
    
    # Compare canonicalized JSONs
    if [ "$expected_canonical" != "$actual_canonical" ]; then
        echo "ERROR: $error_prefix" 
        echo "" 
        echo "Expected (formatted):" 
        echo "$expected_json" | jq . 
        echo "" 
        echo "Actual (formatted):" 
        echo "$actual_json" | jq . 
        echo "" 
        echo "Expected (canonical): $expected_canonical" 
        echo "Actual (canonical):   $actual_canonical" 
        exit 1
    fi
}


echo "=========================================="
echo "Reproducing: AI Attribution Lost During Rebase Conflict Resolution"
echo "=========================================="
echo ""

# Check if git-ai is available
if ! command -v git-ai &> /dev/null; then
    echo "ERROR: git-ai command not found. Please install git-ai first."
    exit 1
fi

# Create temporary directory for test
TEST_DIR=$(mktemp -d)
echo "Test directory: $TEST_DIR"
cd "$TEST_DIR"

# Initialize git repo
git init
git config user.email "test@example.com"
git config user.name "Test User"

# Create initial commit (required for git-ai)
echo "# Test Project" > README.md
git add README.md
git commit -m "Initial commit"


 # Step 1: Create initial file on main branch
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    return 2
EOF
    git-ai checkpoint
    git add shared.py
    git commit -m "Initial shared file"
    
    # Step 2: Create feature branch where AI modifies the file
    git checkout -b feature-ai
    
    # AI modifies function_two and adds new content
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # AI enhanced this function
    result = 2 * 2
    return result
def ai_function():
    print("AI added this")
    return "ai_data"
EOF
    
    git-ai checkpoint mock_ai shared.py
    git add shared.py
    git commit -m "AI enhances function_two and adds ai_function"
    
    # Get stats before conflict resolution
    ai_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq)
    echo "=== AI Stats BEFORE conflict resolution ==="
    
    # Verify AI authorship before conflict
    git-ai blame shared.py | cat

    echo "$ai_stats_before" 
    
    blame_before=$(git-ai blame shared.py)
    [[ "$blame_before" =~ "mock_ai" ]] || {
        echo "ERROR: 'mock_ai' not found in blame output before conflict" 
        return 1
    }

    expected_json='{
        "human_additions": 0,
        "mixed_additions": 0,
        "ai_additions": 6,
        "ai_accepted": 6,
        "total_ai_additions": 6,
        "total_ai_deletions": 1,
        "time_waiting_for_ai": 0,
        "git_diff_deleted_lines": 1,
        "git_diff_added_lines": 6,
        "tool_model_breakdown": {
            "mock_ai::unknown": {
            "ai_additions": 6,
            "mixed_additions": 0,
            "ai_accepted": 6,
            "total_ai_additions": 6,
            "total_ai_deletions": 1,
            "time_waiting_for_ai": 0
            }
        }
    }'

    compare_json "$expected_json" "$ai_stats_before" "AI Stats before conflict resolution do not match expected" || return 1
    
    # Step 3: Go back to main and make conflicting changes
    git checkout main
    
    # Human modifies function_two differently (will cause conflict)
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # Human modified this differently
    value = 2 + 2
    return value
def human_function():
    return "human_data"
EOF
    
    git-ai checkpoint
    git add shared.py
    git commit -m "Human modifies function_two and adds human_function"

    echo "=== Human Stats BEFORE conflict resolution ===" 
    git-ai blame shared.py | cat
    human_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq)
    echo "$human_stats_before"

    expected_json='{
        "human_additions": 5,
        "mixed_additions": 0,
        "ai_additions": 0,
        "ai_accepted": 0,
        "total_ai_additions": 0,
        "total_ai_deletions": 0,
        "time_waiting_for_ai": 0,
        "git_diff_deleted_lines": 1,
        "git_diff_added_lines": 5,
        "tool_model_breakdown": {}
    }'

    compare_json "$expected_json" "$human_stats_before" "Human Stats before conflict do not match expected" || return 1
    
    # Step 4: Attempt rebase - this will cause a conflict
    git checkout feature-ai
    echo "=== Attempting rebase (will conflict) ===" 
    
    # Rebase will stop due to conflict
    if git rebase main 2>&1; then
        echo "ERROR: Expected rebase to fail with conflict, but it succeeded" 
        return 1
    fi
    
    # Verify we're in a conflicted state
    git status 
    
    # Step 5: Resolve the conflict by keeping both changes
    cat > shared.py <<EOF
def function_one():
    return 1
def function_two():
    # AI enhanced this function
    result = 2 * 2
    return result
def ai_function():
    print("AI added this")
    return "ai_data"
def human_function():
    return "human_data"
EOF
    
    # Mark conflict as resolved
    git add shared.py
    
    # Continue rebase (set GIT_EDITOR to bypass interactive editor)
    echo "=== Continuing rebase after conflict resolution ===" 
    GIT_EDITOR=true git rebase --continue
    
    # Step 6: Verify AI authorship is preserved after conflict resolution
    echo "=== Stats AFTER conflict resolution ===" 
    git-ai blame shared.py | cat
    feature_commit_after=$(git rev-parse HEAD)
    stats_after=$(git-ai stats --json "$feature_commit_after" | jq)
    echo "$stats_after" 
    
    
    # The stats should show AI additions preserved after conflict resolution
    # AI added 6 lines (function_two enhancement: 3 lines, ai_function: 3 lines)
    # Note: The conflict resolution shows deletions from both human and AI sides
    expected_json='{
      "human_additions": 0,
      "mixed_additions": 0,
      "ai_additions": 6,
      "ai_accepted": 6,
      "total_ai_additions": 6,
      "total_ai_deletions": 1,
      "time_waiting_for_ai": 0,
      "git_diff_deleted_lines": 3,
      "git_diff_added_lines": 6,
      "tool_model_breakdown": {
        "mock_ai::unknown": {
          "ai_additions": 6,
          "mixed_additions": 0,
          "ai_accepted": 6,
          "total_ai_additions": 6,
          "total_ai_deletions": 1,
          "time_waiting_for_ai": 0
        }
      }
    }'
    
    compare_json "$expected_json" "$stats_after" "Stats after conflict resolution do not match expected" || return 1
    
    # Verify blame shows AI authorship for AI lines
    echo "=== Blame AFTER conflict resolution ===" 
    git-ai blame shared.py | cat
    # Show the diff for informational purposes
    echo "=== Git Diff after conflict resolution ===" 
    git diff HEAD^ HEAD -- shared.py 
    
    blame_after=$(git-ai blame shared.py)
    
    # Verify AI attribution is present
    [[ "$blame_after" =~ "mock_ai" ]] || {
        echo "ERROR: 'mock_ai' not found in blame output after conflict resolution" 
        echo "AI authorship was NOT preserved after conflict resolution!" 
        return 1
    }
    
    # Verify human attribution is also present
    [[ "$blame_after" =~ "Test User" ]] || {
        echo "ERROR: 'Test User' not found in blame output after conflict resolution" 
        return 1
    }
    
    # Verify the file has expected content
    grep -q "AI added this" shared.py || {
        echo "ERROR: AI content not found in resolved file" 
        return 1
    }
    
    grep -q "human_data" shared.py || {
        echo "ERROR: Human content not found in resolved file" 
        return 1
    }
    
    echo "✓ AI attribution successfully preserved after conflict resolution during rebase" 
<!-- gh-comment-id:3482484005 --> @AtnesNess commented on GitHub (Nov 3, 2025): I still see an issue: Stats incorrectly show: "ai_additions": 8 ``` ➜ git-ai -v 1.0.15 ``` LOG ``` bash test.sh ========================================== Reproducing: AI Attribution Lost During Rebase Conflict Resolution ========================================== Test directory: /tmp/tmp.gNTVVPXu9G Initialized empty Git repository in /tmp/tmp.gNTVVPXu9G/.git/ [main (root-commit) 193758c] Initial commit 1 file changed, 1 insertion(+) create mode 100644 README.md you ████████████████████████████████████████ ai 100% 0% human Test User changed 1 file(s) that have changed since the last commit Checkpoint completed in 27.180889ms [main 68b89bb] Initial shared file 1 file changed, 4 insertions(+) create mode 100644 shared.py you ████████████████████████████████████████ ai 100% 0% Switched to a new branch 'feature-ai' ai_agent mock_ai changed 1 file(s) that have changed since the last commit Checkpoint completed in 37.02687ms [feature-ai 5cd7aa0] AI enhances function_two and adds ai_function 1 file changed, 6 insertions(+), 1 deletion(-) you ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ai 0% 100% 100% AI code accepted [DEBUG] Stats command found commit: 5cd7aa0e9992243187cd0465202ca1b446478c26 refname: 5cd7aa0e9992243187cd0465202ca1b446478c26 === AI Stats BEFORE conflict resolution === 68b89bb (Test User 2025-11-03 20:31:23 +0000 1) def function_one(): 68b89bb (Test User 2025-11-03 20:31:23 +0000 2) return 1 68b89bb (Test User 2025-11-03 20:31:23 +0000 3) def function_two(): 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 4) # AI enhanced this function 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 5) result = 2 * 2 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 6) return result 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 7) def ai_function(): 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 8) print("AI added this") 5cd7aa0 (mock_ai 2025-11-03 20:31:23 +0000 9) return "ai_data" { "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } } Switched to branch 'main' human Test User changed 1 file(s) that have changed since the last commit Checkpoint completed in 36.592686ms [main 8b8b4cf] Human modifies function_two and adds human_function 1 file changed, 5 insertions(+), 1 deletion(-) you ████████████████████████████████████████ ai 100% 0% === Human Stats BEFORE conflict resolution === 68b89bb (Test User 2025-11-03 20:31:23 +0000 1) def function_one(): 68b89bb (Test User 2025-11-03 20:31:23 +0000 2) return 1 68b89bb (Test User 2025-11-03 20:31:23 +0000 3) def function_two(): 8b8b4cf (Test User 2025-11-03 20:31:23 +0000 4) # Human modified this differently 8b8b4cf (Test User 2025-11-03 20:31:23 +0000 5) value = 2 + 2 8b8b4cf (Test User 2025-11-03 20:31:23 +0000 6) return value 8b8b4cf (Test User 2025-11-03 20:31:23 +0000 7) def human_function(): 8b8b4cf (Test User 2025-11-03 20:31:23 +0000 8) return "human_data" [DEBUG] Stats command found commit: 8b8b4cf5aa195f298d5cd26d6e9a4b299a4a4232 refname: 8b8b4cf5aa195f298d5cd26d6e9a4b299a4a4232 { "human_additions": 5, "mixed_additions": 0, "ai_additions": 0, "ai_accepted": 0, "total_ai_additions": 0, "total_ai_deletions": 0, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 5, "tool_model_breakdown": {} } Switched to branch 'feature-ai' === Attempting rebase (will conflict) === Auto-merging shared.py CONFLICT (content): Merge conflict in shared.py error: could not apply 5cd7aa0... AI enhances function_two and adds ai_function hint: Resolve all conflicts manually, mark them as resolved with hint: "git add/rm <conflicted_files>", then run "git rebase --continue". hint: You can instead skip this commit: run "git rebase --skip". hint: To abort and get back to the state before "git rebase", run "git rebase --abort". hint: Disable this message with "git config set advice.mergeConflict false" Could not apply 5cd7aa0... # AI enhances function_two and adds ai_function interactive rebase in progress; onto 8b8b4cf Last command done (1 command done): pick 5cd7aa0 # AI enhances function_two and adds ai_function No commands remaining. You are currently rebasing branch 'feature-ai' on '8b8b4cf'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged <file>..." to unstage) (use "git add <file>..." to mark resolution) both modified: shared.py no changes added to commit (use "git add" and/or "git commit -a") === Continuing rebase after conflict resolution === [detached HEAD 7b8965e] AI enhances function_two and adds ai_function 1 file changed, 6 insertions(+), 3 deletions(-) Successfully rebased and updated refs/heads/feature-ai. === Stats AFTER conflict resolution === 68b89bb (Test User 2025-11-03 20:31:23 +0000 1) def function_one(): 68b89bb (Test User 2025-11-03 20:31:23 +0000 2) return 1 68b89bb (Test User 2025-11-03 20:31:23 +0000 3) def function_two(): 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 4) # AI enhanced this function 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 5) result = 2 * 2 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 6) return result 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 7) def ai_function(): 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 8) print("AI added this") 7b8965e (mock_ai 2025-11-03 20:31:23 +0000 9) return "ai_data" 8b8b4cf (mock_ai 2025-11-03 20:31:23 +0000 10) def human_function(): 8b8b4cf (mock_ai 2025-11-03 20:31:23 +0000 11) return "human_data" [DEBUG] Stats command found commit: 7b8965e2c1ef0cfaca153e8c0f3d5385bc97922b refname: 7b8965e2c1ef0cfaca153e8c0f3d5385bc97922b { "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 8, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 8, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } } ERROR: Stats after conflict resolution do not match expected Expected (formatted): { "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } } Actual (formatted): { "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 8, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 8, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } } Expected (canonical): {"ai_accepted":6,"ai_additions":6,"git_diff_added_lines":6,"git_diff_deleted_lines":3,"human_additions":0,"mixed_additions":0,"time_waiting_for_ai":0,"tool_model_breakdown":{"mock_ai::unknown":{"ai_accepted":6,"ai_additions":6,"mixed_additions":0,"time_waiting_for_ai":0,"total_ai_additions":6,"total_ai_deletions":1}},"total_ai_additions":6,"total_ai_deletions":1} Actual (canonical): {"ai_accepted":8,"ai_additions":6,"git_diff_added_lines":6,"git_diff_deleted_lines":3,"human_additions":0,"mixed_additions":0,"time_waiting_for_ai":0,"tool_model_breakdown":{"mock_ai::unknown":{"ai_accepted":8,"ai_additions":6,"mixed_additions":0,"time_waiting_for_ai":0,"total_ai_additions":6,"total_ai_deletions":1}},"total_ai_additions":6,"total_ai_deletions":1} ``` Script ``` #!/bin/bash set -e # Reproduction script for: # "AI attribution is preserved after fixing conflict during rebase" # # This script validates that git-ai preserves AI authorship information # when resolving merge conflicts during a rebase operation. # # Expected behavior: AI attribution should be preserved after conflict resolution # Actual behavior: AI attribution may be lost or incorrectly attributed # Returns: 0 if JSONs match, 1 otherwise compare_json() { local expected_json="$1" local actual_json="$2" local error_prefix="${3:-JSON mismatch}" # Check if jq is available if ! command -v jq &> /dev/null; then echo "WARNING: jq not available, skipping JSON comparison" return 0 fi # Verify both inputs are valid JSON if ! echo "$expected_json" | jq . >/dev/null 2>&1; then echo "ERROR: Expected JSON is invalid" return 1 fi if ! echo "$actual_json" | jq . >/dev/null 2>&1; then echo "ERROR: Actual JSON is invalid" echo "Actual: $actual_json" return 1 fi # Canonicalize both JSONs (sort keys, compact format) local expected_canonical=$(echo "$expected_json" | jq -cS .) local actual_canonical=$(echo "$actual_json" | jq -cS .) # Compare canonicalized JSONs if [ "$expected_canonical" != "$actual_canonical" ]; then echo "ERROR: $error_prefix" echo "" echo "Expected (formatted):" echo "$expected_json" | jq . echo "" echo "Actual (formatted):" echo "$actual_json" | jq . echo "" echo "Expected (canonical): $expected_canonical" echo "Actual (canonical): $actual_canonical" exit 1 fi } echo "==========================================" echo "Reproducing: AI Attribution Lost During Rebase Conflict Resolution" echo "==========================================" echo "" # Check if git-ai is available if ! command -v git-ai &> /dev/null; then echo "ERROR: git-ai command not found. Please install git-ai first." exit 1 fi # Create temporary directory for test TEST_DIR=$(mktemp -d) echo "Test directory: $TEST_DIR" cd "$TEST_DIR" # Initialize git repo git init git config user.email "test@example.com" git config user.name "Test User" # Create initial commit (required for git-ai) echo "# Test Project" > README.md git add README.md git commit -m "Initial commit" # Step 1: Create initial file on main branch cat > shared.py <<EOF def function_one(): return 1 def function_two(): return 2 EOF git-ai checkpoint git add shared.py git commit -m "Initial shared file" # Step 2: Create feature branch where AI modifies the file git checkout -b feature-ai # AI modifies function_two and adds new content cat > shared.py <<EOF def function_one(): return 1 def function_two(): # AI enhanced this function result = 2 * 2 return result def ai_function(): print("AI added this") return "ai_data" EOF git-ai checkpoint mock_ai shared.py git add shared.py git commit -m "AI enhances function_two and adds ai_function" # Get stats before conflict resolution ai_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq) echo "=== AI Stats BEFORE conflict resolution ===" # Verify AI authorship before conflict git-ai blame shared.py | cat echo "$ai_stats_before" blame_before=$(git-ai blame shared.py) [[ "$blame_before" =~ "mock_ai" ]] || { echo "ERROR: 'mock_ai' not found in blame output before conflict" return 1 } expected_json='{ "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } }' compare_json "$expected_json" "$ai_stats_before" "AI Stats before conflict resolution do not match expected" || return 1 # Step 3: Go back to main and make conflicting changes git checkout main # Human modifies function_two differently (will cause conflict) cat > shared.py <<EOF def function_one(): return 1 def function_two(): # Human modified this differently value = 2 + 2 return value def human_function(): return "human_data" EOF git-ai checkpoint git add shared.py git commit -m "Human modifies function_two and adds human_function" echo "=== Human Stats BEFORE conflict resolution ===" git-ai blame shared.py | cat human_stats_before=$(git-ai stats --json "$(git rev-parse HEAD)" | jq) echo "$human_stats_before" expected_json='{ "human_additions": 5, "mixed_additions": 0, "ai_additions": 0, "ai_accepted": 0, "total_ai_additions": 0, "total_ai_deletions": 0, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 1, "git_diff_added_lines": 5, "tool_model_breakdown": {} }' compare_json "$expected_json" "$human_stats_before" "Human Stats before conflict do not match expected" || return 1 # Step 4: Attempt rebase - this will cause a conflict git checkout feature-ai echo "=== Attempting rebase (will conflict) ===" # Rebase will stop due to conflict if git rebase main 2>&1; then echo "ERROR: Expected rebase to fail with conflict, but it succeeded" return 1 fi # Verify we're in a conflicted state git status # Step 5: Resolve the conflict by keeping both changes cat > shared.py <<EOF def function_one(): return 1 def function_two(): # AI enhanced this function result = 2 * 2 return result def ai_function(): print("AI added this") return "ai_data" def human_function(): return "human_data" EOF # Mark conflict as resolved git add shared.py # Continue rebase (set GIT_EDITOR to bypass interactive editor) echo "=== Continuing rebase after conflict resolution ===" GIT_EDITOR=true git rebase --continue # Step 6: Verify AI authorship is preserved after conflict resolution echo "=== Stats AFTER conflict resolution ===" git-ai blame shared.py | cat feature_commit_after=$(git rev-parse HEAD) stats_after=$(git-ai stats --json "$feature_commit_after" | jq) echo "$stats_after" # The stats should show AI additions preserved after conflict resolution # AI added 6 lines (function_two enhancement: 3 lines, ai_function: 3 lines) # Note: The conflict resolution shows deletions from both human and AI sides expected_json='{ "human_additions": 0, "mixed_additions": 0, "ai_additions": 6, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0, "git_diff_deleted_lines": 3, "git_diff_added_lines": 6, "tool_model_breakdown": { "mock_ai::unknown": { "ai_additions": 6, "mixed_additions": 0, "ai_accepted": 6, "total_ai_additions": 6, "total_ai_deletions": 1, "time_waiting_for_ai": 0 } } }' compare_json "$expected_json" "$stats_after" "Stats after conflict resolution do not match expected" || return 1 # Verify blame shows AI authorship for AI lines echo "=== Blame AFTER conflict resolution ===" git-ai blame shared.py | cat # Show the diff for informational purposes echo "=== Git Diff after conflict resolution ===" git diff HEAD^ HEAD -- shared.py blame_after=$(git-ai blame shared.py) # Verify AI attribution is present [[ "$blame_after" =~ "mock_ai" ]] || { echo "ERROR: 'mock_ai' not found in blame output after conflict resolution" echo "AI authorship was NOT preserved after conflict resolution!" return 1 } # Verify human attribution is also present [[ "$blame_after" =~ "Test User" ]] || { echo "ERROR: 'Test User' not found in blame output after conflict resolution" return 1 } # Verify the file has expected content grep -q "AI added this" shared.py || { echo "ERROR: AI content not found in resolved file" return 1 } grep -q "human_data" shared.py || { echo "ERROR: Human content not found in resolved file" return 1 } echo "✓ AI attribution successfully preserved after conflict resolution during rebase" ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/git-ai#64
No description provided.