Compare commits

...

3 Commits

Author SHA1 Message Date
openhands
aedd795aff Perfect the git changes solution - fix 'clear after push' behavior
- Only use merge-base when origin/current-branch is actually behind (would show merged content)
- This ensures that after pushing changes, the changes tab is clear
- While still preventing merged content from appearing as user changes
- Addresses the exact user scenario: clear after push, filtered after merge
2025-07-22 17:33:16 +00:00
openhands
0e19d3b723 Improve git changes solution to fix regression
- Add smart merge detection to only use merge-base when recent merges are detected
- This preserves the original behavior (clear after push) while fixing the merge issue
- Add comprehensive tests for both scenarios
- Fixes regression where changes would always show after push
2025-07-22 17:11:09 +00:00
openhands
324d7bff0f Fix git changes tab showing merged content as user changes
- Change reference priority in GitHandler._get_valid_ref() to prefer merge-base over origin/current-branch
- This prevents merged content from appearing as new user changes in the changes tab
- Update tests to reflect new expected behavior
- Add test case for merge scenario to prevent regression

Fixes issue where changes tab would show all merged changes from main as if they were new user changes after merging main into a feature branch.
2025-07-22 16:35:59 +00:00
2 changed files with 108 additions and 14 deletions

View File

@@ -75,10 +75,38 @@ class GitHandler:
output = self.execute(cmd, self.cwd)
return output.exit_code == 0
def _has_recent_merge_commits(self) -> bool:
"""
Checks if there are recent merge commits in the current branch.
Returns:
bool: True if there are merge commits in the last 10 commits, False otherwise.
"""
# Check for merge commits in recent history
cmd = 'git --no-pager rev-list --merges -n 1 HEAD~10..HEAD 2>/dev/null || git --no-pager rev-list --merges -n 1 HEAD'
output = self.execute(cmd, self.cwd)
has_merges = output.exit_code == 0 and output.content.strip() != ''
if has_merges:
return True
# Also check if HEAD itself is a merge commit
cmd = 'git --no-pager rev-list --parents -n 1 HEAD'
output = self.execute(cmd, self.cwd)
if output.exit_code == 0:
# If HEAD has more than one parent, it's a merge commit
parents = output.content.strip().split()
return len(parents) > 2 # commit hash + parent1 + parent2 (or more)
return False
def _get_valid_ref(self) -> str | None:
"""
Determines a valid Git reference for comparison.
Uses merge-base when recent merge commits are detected to show only user changes.
Otherwise uses origin/current-branch to show changes since last push.
Returns:
str | None: A valid Git reference or None if no valid reference is found.
"""
@@ -86,16 +114,33 @@ class GitHandler:
default_branch = self._get_default_branch()
ref_current_branch = f'origin/{current_branch}'
ref_non_default_branch = f'$(git --no-pager merge-base HEAD "$(git --no-pager rev-parse --abbrev-ref origin/{default_branch})")'
ref_merge_base = f'$(git --no-pager merge-base HEAD "$(git --no-pager rev-parse --abbrev-ref origin/{default_branch})")'
ref_default_branch = 'origin/' + default_branch
ref_new_repo = '$(git --no-pager rev-parse --verify 4b825dc642cb6eb9a060e54bf8d69288fbee4904)' # compares with empty tree
# Only use merge-base if:
# 1. There are recent merge commits AND
# 2. origin/current-branch would show merged content (is behind after merge)
if (
self._has_recent_merge_commits()
and self._verify_ref_exists(ref_current_branch)
and self._verify_ref_exists(ref_merge_base)
):
# Check if origin/current-branch is behind (would show merged content)
cmd = f'git --no-pager rev-list --count {ref_current_branch}..HEAD'
output = self.execute(cmd, self.cwd)
if output.exit_code == 0 and int(output.content.strip()) > 0:
# origin/current-branch is behind, use merge-base to filter merged content
return ref_merge_base
# Default behavior: use origin/current-branch first (shows changes since push)
refs = [
ref_current_branch,
ref_non_default_branch,
ref_merge_base,
ref_default_branch,
ref_new_repo,
]
for ref in refs:
if self._verify_ref_exists(ref):
return ref

View File

@@ -164,22 +164,12 @@ class TestGitHandler(unittest.TestCase):
)
def test_get_valid_ref_with_origin_current_branch(self):
"""Test that _get_valid_ref returns the current branch in origin when it exists."""
"""Test that _get_valid_ref returns origin/current-branch when no recent merges."""
# This test uses the setup from setUp where the current branch exists in origin
ref = self.git_handler._get_valid_ref()
self.assertIsNotNone(ref)
# Check that the refs were checked in the correct order
verify_commands = [
cmd
for cmd, _ in self.executed_commands
if cmd.startswith('git --no-pager rev-parse --verify')
]
# First should check origin/feature-branch (current branch)
self.assertTrue(any('origin/feature-branch' in cmd for cmd in verify_commands))
# Should have found a valid ref (origin/feature-branch)
# Should prefer origin/feature-branch when no recent merge commits
self.assertEqual(ref, 'origin/feature-branch')
# Verify the ref exists
@@ -396,6 +386,65 @@ class TestGitHandler(unittest.TestCase):
)
)
def test_merge_scenario_shows_only_user_changes(self):
"""Test that after merging main, only user changes are shown, not merged content."""
# Add a file to main in origin (simulating other developers' work)
with open(os.path.join(self.origin_dir, 'main-addition.txt'), 'w') as f:
f.write('Content added to main')
self._execute_command('git add main-addition.txt', self.origin_dir)
self._execute_command("git commit -m 'Add main-addition.txt'", self.origin_dir)
# Switch back to local repo and merge main
self._execute_command('git fetch origin', self.local_dir)
self._execute_command('git merge origin/main', self.local_dir)
# Clear executed commands to start fresh
self.executed_commands = []
# Get changes after merge
changes = self.git_handler.get_git_changes()
self.assertIsNotNone(changes)
# Should NOT include the merged file as a change
change_paths = [change['path'] for change in changes]
self.assertNotIn(
'main-addition.txt',
change_paths,
'Merged content should not appear as user changes',
)
# Should still show user's original changes (if any exist relative to merge-base)
# In this case, we expect to see the files that were changed on the feature branch
# relative to where it diverged from main
def test_no_changes_after_push_without_merge(self):
"""Test that after pushing changes (without merge), no changes are shown."""
# Create a new file and commit it
with open(os.path.join(self.local_dir, 'new-feature.txt'), 'w') as f:
f.write('New feature content')
self._execute_command('git add new-feature.txt', self.local_dir)
self._execute_command("git commit -m 'Add new feature'", self.local_dir)
# Push the changes
self._execute_command('git push origin feature-branch', self.local_dir)
# Clear executed commands to start fresh
self.executed_commands = []
# Get changes after push - should show no changes since no recent merges
changes = self.git_handler.get_git_changes()
self.assertIsNotNone(changes)
# Should show no changes since everything is pushed and no recent merges
# This tests the regression fix - after push, changes should be clear
self.assertEqual(
len(changes),
0,
'Should show no changes after pushing without recent merges',
)
if __name__ == '__main__':
unittest.main()