From 8f9bdad87316783dc4c8921d5abe00bbe1f3ffbe Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Tue, 7 Apr 2026 19:28:07 +0100 Subject: [PATCH 1/8] feat(ADFA-2885): Set up merge detection functions --- .../itsaky/androidide/git/core/GitRepository.kt | 5 +++++ .../androidide/git/core/JGitRepository.kt | 17 ++++++++++++++++- .../androidide/git/core/models/GitStatus.kt | 2 ++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/GitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/GitRepository.kt index 917eab54ab..539d98d34b 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/GitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/GitRepository.kt @@ -3,6 +3,7 @@ package com.itsaky.androidide.git.core import com.itsaky.androidide.git.core.models.GitBranch import com.itsaky.androidide.git.core.models.GitCommit import com.itsaky.androidide.git.core.models.GitStatus +import org.eclipse.jgit.api.MergeResult import org.eclipse.jgit.api.PullResult import org.eclipse.jgit.lib.ProgressMonitor import org.eclipse.jgit.transport.CredentialsProvider @@ -41,4 +42,8 @@ interface GitRepository : Closeable { credentialsProvider: CredentialsProvider? = null, progressMonitor: ProgressMonitor? = null ): PullResult + + // Merge Operations + suspend fun merge(branchName: String): MergeResult + suspend fun abortMerge() } diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt index 0891e0a1af..008baaf57e 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.ListBranchCommand.ListMode +import org.eclipse.jgit.api.MergeResult import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.dircache.DirCacheIterator import org.eclipse.jgit.lib.BranchConfig @@ -22,6 +23,8 @@ import org.eclipse.jgit.storage.file.FileRepositoryBuilder import org.eclipse.jgit.transport.CredentialsProvider import org.eclipse.jgit.transport.PushResult import org.eclipse.jgit.api.PullResult +import org.eclipse.jgit.api.ResetCommand.ResetType +import org.eclipse.jgit.lib.RepositoryState import org.eclipse.jgit.treewalk.AbstractTreeIterator import org.eclipse.jgit.treewalk.CanonicalTreeParser import org.eclipse.jgit.treewalk.EmptyTreeIterator @@ -76,11 +79,12 @@ class JGitRepository(override val rootDir: File) : GitRepository { jgitStatus.conflicting.forEach { conflicted.add(FileChange(it, ChangeType.CONFLICTED)) } - + val isMerging = repository.repositoryState == RepositoryState.MERGING GitStatus( isClean = jgitStatus.isClean, hasConflicts = jgitStatus.conflicting.isNotEmpty(), + isMerging = isMerging, staged = staged, unstaged = unstaged, untracked = untracked, @@ -272,6 +276,17 @@ class JGitRepository(override val rootDir: File) : GitRepository { pullCommand.call() } + override suspend fun merge(branchName: String): MergeResult = withContext(Dispatchers.IO) { + val branchRef = repository.findRef(branchName) ?: throw IllegalArgumentException("Branch $branchName not found") + git.merge().include(branchRef).call() + } + + override suspend fun abortMerge(): Unit = withContext(Dispatchers.IO) { + if (repository.repositoryState == RepositoryState.MERGING) { + git.reset().setMode(ResetType.HARD).call() + } + } + override fun close() { repository.close() git.close() diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/models/GitStatus.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/models/GitStatus.kt index c181510215..4aa57c2a84 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/models/GitStatus.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/models/GitStatus.kt @@ -6,6 +6,7 @@ package com.itsaky.androidide.git.core.models data class GitStatus( val isClean: Boolean, val hasConflicts: Boolean, + val isMerging: Boolean, val staged: List, val unstaged: List, val untracked: List, @@ -15,6 +16,7 @@ data class GitStatus( val EMPTY = GitStatus( isClean = true, hasConflicts = false, + isMerging = false, staged = emptyList(), unstaged = emptyList(), untracked = emptyList(), From 8d8d1c42fe1b4c66706e69548e488f0480b127d3 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Wed, 8 Apr 2026 22:05:07 +0100 Subject: [PATCH 2/8] feat(ADFA-2885): Notify about merge conflicts --- .../fragments/git/GitBottomSheetFragment.kt | 24 +++++++++++++++---- .../viewmodel/GitBottomSheetViewModel.kt | 20 +++++++++++++--- resources/src/main/res/values/strings.xml | 3 +++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index 12e17b154a..6823d551b6 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -22,6 +22,7 @@ import com.itsaky.androidide.git.core.GitCredentialsManager import com.itsaky.androidide.preferences.internal.GitPreferences import com.itsaky.androidide.utils.flashSuccess import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel.PullUiState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @@ -214,25 +215,38 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { viewLifecycleOwner.lifecycleScope.launch { viewModel.pullState.collectLatest { state -> when (state) { - is GitBottomSheetViewModel.PullUiState.Idle -> { + is PullUiState.Idle -> { binding.btnPull.isEnabled = true binding.pullProgress.visibility = View.GONE } - is GitBottomSheetViewModel.PullUiState.Pulling -> { + is PullUiState.Pulling -> { binding.btnPull.isEnabled = false binding.pullProgress.visibility = View.VISIBLE } - is GitBottomSheetViewModel.PullUiState.Success -> { + is PullUiState.Success -> { binding.btnPull.isEnabled = true binding.pullProgress.visibility = View.GONE flashSuccess(R.string.pull_successful) viewModel.resetPullState() } - is GitBottomSheetViewModel.PullUiState.Error -> { + is PullUiState.Conflicts -> { + binding.btnPull.isEnabled = true + binding.pullProgress.visibility = View.GONE + val message = state.message ?: getString(R.string.info_merge_conflicts) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.merge_conflicts)) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show() + viewModel.resetPullState() + } + is PullUiState.Error -> { binding.btnPull.isEnabled = true binding.pullProgress.visibility = View.GONE val message = - state.errorResId?.let { getString(it) } ?: state.message + state.message ?: state.errorResId?.let { resId -> + if (state.errorArgs != null) getString(resId, *state.errorArgs.toTypedArray()) else getString(resId) + } MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.pull_failed) .setMessage(message) diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index 40bed1d722..5461da2cab 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -22,8 +22,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.eclipse.jgit.api.MergeResult.MergeStatus import org.eclipse.jgit.api.PullResult import org.eclipse.jgit.errors.NoRemoteRepositoryException +import org.eclipse.jgit.api.errors.CheckoutConflictException import org.eclipse.jgit.transport.RemoteRefUpdate import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider import org.greenrobot.eventbus.EventBus @@ -256,6 +258,10 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana } handlePullSuccess(username, token) + } catch (e: CheckoutConflictException) { + log.error("Pull failed with checkout conflict", e) + val paths = e.conflictingPaths?.joinToString("\n") ?: "" + _pullState.value = PullUiState.Error(errorResId = R.string.checkout_conflict_message, errorArgs = listOf(paths)) } catch (e: Exception) { log.error("Pull failed", e) if (e.message?.contains("not authorized", ignoreCase = true) == true) { @@ -274,8 +280,15 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana } private fun handlePullError(result: PullResult) { - val status = result.mergeResult?.mergeStatus?.name ?: "Unknown error" - _pullState.value = PullUiState.Error("Pull failed: $status") + val mergeStatus = result.mergeResult?.mergeStatus + val statusName = mergeStatus?.name ?: "Unknown error" + + if (mergeStatus == MergeStatus.CONFLICTING) { + _pullState.value = PullUiState.Conflicts() + refreshStatus() + } else { + _pullState.value = PullUiState.Error("Pull failed: $statusName") + } } private fun handlePullSuccess( @@ -302,7 +315,8 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana object Idle : PullUiState() object Pulling : PullUiState() object Success : PullUiState() - data class Error(val message: String? = null, val errorResId: Int? = R.string.unknown_error) : PullUiState() + data class Conflicts(val message: String? = null) : PullUiState() + data class Error(val message: String? = null, val errorResId: Int? = R.string.unknown_error, val errorArgs: List? = null) : PullUiState() } sealed class PushUiState { diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index f5066ae99c..d678cd0b45 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -619,6 +619,7 @@ Warning GitHub + Checkout conflict: Please commit or stash your local changes before pulling. Conflicting files:\n%1$s Choose a different location New directory name Error @@ -1275,4 +1276,6 @@ Enter your credentials to sync changes with the remote repository. Clear Credentials Authentication failed. Ensure your Personal Access Token has \'repo\' permissions, or try clearing and re-entering credentials. + Pull completed with conflicts!\nPlease select the conflicted files from the list, resolve the highlighted issues in the editor, save file(s) and then commit your changes. + Merge Conflicts From 578dbeab9e99c61cc59cc872dbe35baa0da1152b Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Wed, 8 Apr 2026 22:17:09 +0100 Subject: [PATCH 3/8] feat(ADFA-2885): Resolve merge conflicts in editor --- .../fragments/git/GitBottomSheetFragment.kt | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index 6823d551b6..ce928322e5 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -12,25 +12,31 @@ import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.itsaky.androidide.R import com.itsaky.androidide.activities.PreferencesActivity +import com.itsaky.androidide.activities.editor.EditorHandlerActivity import com.itsaky.androidide.databinding.DialogGitCredentialsBinding import com.itsaky.androidide.databinding.FragmentGitBottomSheetBinding import com.itsaky.androidide.fragments.git.adapter.GitFileChangeAdapter import com.itsaky.androidide.git.core.GitCredentialsManager +import com.itsaky.androidide.git.core.models.ChangeType import com.itsaky.androidide.preferences.internal.GitPreferences import com.itsaky.androidide.utils.flashSuccess +import com.itsaky.androidide.viewmodel.BottomSheetViewModel import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel.PullUiState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.activityViewModel +import java.io.File class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { private val viewModel: GitBottomSheetViewModel by activityViewModel() + private val bottomSheetViewModel: BottomSheetViewModel by activityViewModel() private lateinit var fileChangeAdapter: GitFileChangeAdapter private lateinit var credentialsManager: GitCredentialsManager @@ -44,9 +50,26 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { fileChangeAdapter = GitFileChangeAdapter( onFileClicked = { change -> - // Show diff in a dialog when changed file is clicked - val dialog = GitDiffViewerDialog.newInstance(change.path) - dialog.show(childFragmentManager, "GitDiffViewerDialog") + when (change.type) { + ChangeType.CONFLICTED -> { + // Open conflicted file in editor + val activity = requireActivity() + if (activity is EditorHandlerActivity) { + viewLifecycleOwner.lifecycleScope.launch { + val repo = viewModel.currentRepository + repo?.let { + activity.openFile(File(repo.rootDir, change.path)) + bottomSheetViewModel.setSheetState(BottomSheetBehavior.STATE_COLLAPSED) + } + } + } + } + else -> { + // Show diff in a dialog when changed file is clicked + val dialog = GitDiffViewerDialog.newInstance(change.path) + dialog.show(childFragmentManager, "GitDiffViewerDialog") + } + } }, onSelectionChanged = { validateCommitButton() From d97106c2dc306c0b00840a9bac96d5545db43439 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Wed, 8 Apr 2026 22:32:08 +0100 Subject: [PATCH 4/8] fix(ADFA-2885): Update open conflicted file with conflict markers --- .../androidide/activities/editor/EditorHandlerActivity.kt | 3 ++- .../itsaky/androidide/fragments/git/GitBottomSheetFragment.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index 6c6315cfae..1362abca90 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -271,7 +271,7 @@ open class EditorHandlerActivity : invalidateOptionsMenu() } - private fun checkForExternalFileChanges() { + fun checkForExternalFileChanges() { // Get the list of files currently managed by the ViewModel val openFiles = editorViewModel.getOpenedFiles() if (openFiles.isEmpty() || fileTimestamps.isEmpty()) return @@ -292,6 +292,7 @@ open class EditorHandlerActivity : editorView.editor?.setText(newContent) editorView.markAsSaved() + fileTimestamps[file.absolutePath] = currentTimestamp updateTabs() } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index ce928322e5..59e4298464 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -58,6 +58,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { viewLifecycleOwner.lifecycleScope.launch { val repo = viewModel.currentRepository repo?.let { + activity.checkForExternalFileChanges() activity.openFile(File(repo.rootDir, change.path)) bottomSheetViewModel.setSheetState(BottomSheetBehavior.STATE_COLLAPSED) } From b096948dc9801f171231ee7e4571ad971c8490db Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Wed, 8 Apr 2026 23:26:29 +0100 Subject: [PATCH 5/8] feat(ADFA-2885): Handle conflict when pushing --- .../androidide/fragments/git/GitCommitHistoryDialog.kt | 7 +++++-- .../androidide/viewmodel/GitBottomSheetViewModel.kt | 8 +++++++- resources/src/main/res/values/strings.xml | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitCommitHistoryDialog.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitCommitHistoryDialog.kt index 824de50c0e..841ff279cc 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitCommitHistoryDialog.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitCommitHistoryDialog.kt @@ -132,8 +132,11 @@ class GitCommitHistoryDialog : DialogFragment() { is GitBottomSheetViewModel.PushUiState.Error -> { binding.btnPush.isEnabled = true binding.pushProgress.visibility = View.GONE - val message = - state.errorResId?.let { getString(it) } ?: state.message + val message = if (state.errorResId != null && state.errorResId != R.string.unknown_error) { + getString(state.errorResId) + } else { + state.message ?: getString(R.string.unknown_error) + } MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.push_failed) .setMessage(message) diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index 5461da2cab..dd35a89b34 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -221,8 +221,14 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana } else null private fun handlePushError(update: RemoteRefUpdate) { + val resId = if (update.status == RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD) { + R.string.push_rejected_nonfastforward + } else { + R.string.unknown_error + } _pushState.value = PushUiState.Error( - update.message ?: update.status.name + message = update.message ?: update.status.name, + errorResId = resId ) } diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index d678cd0b45..2339b83494 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1268,6 +1268,7 @@ Pushing… Push successful! Push failed + Push rejected: Your branch is behind the remote. Please pull to resolve conflicts before pushing. Pull Pulling… Pull successful! From 3aafb64120bff681e65c2dfad89a4cfcf3a84e5b Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Thu, 9 Apr 2026 00:01:16 +0100 Subject: [PATCH 6/8] feat(ADFA-2885): Abort merge --- .../fragments/git/GitBottomSheetFragment.kt | 19 +++++++++++++++++++ .../viewmodel/GitBottomSheetViewModel.kt | 12 ++++++++++++ .../res/layout/fragment_git_bottom_sheet.xml | 14 +++++++++++++- resources/src/main/res/values/strings.xml | 2 ++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index 59e4298464..ee4a8086f3 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -106,6 +106,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { commitSection.visibility = View.GONE authorWarning.visibility = View.GONE commitHistoryButton.visibility = View.GONE + btnAbortMerge.visibility = View.GONE } allChanges.isEmpty() -> binding.apply { emptyView.visibility = View.VISIBLE @@ -114,6 +115,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { commitSection.visibility = View.GONE authorWarning.visibility = View.GONE commitHistoryButton.visibility = View.VISIBLE + btnAbortMerge.visibility = View.GONE } else -> { binding.apply { @@ -122,6 +124,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { commitSection.visibility = View.VISIBLE authorWarning.visibility = if (hasAuthorInfo()) View.GONE else View.VISIBLE commitHistoryButton.visibility = View.VISIBLE + btnAbortMerge.visibility = if (status.isMerging) View.VISIBLE else View.GONE } fileChangeAdapter.submitList(allChanges) } @@ -159,6 +162,22 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitSummary.doAfterTextChanged { validateCommitButton() } binding.commitDescription.doAfterTextChanged { validateCommitButton() } + binding.btnAbortMerge.setOnClickListener { + val dialog = MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.abort_merge) + .setMessage(R.string.confirm_abort_merge) + .setPositiveButton(R.string.abort_merge) { _, _ -> + viewModel.abortMerge { + val activity = requireActivity() + if (activity is EditorHandlerActivity) { + activity.checkForExternalFileChanges() + } + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + binding.authorAvatar.setOnClickListener { showAuthorPopup() } diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index dd35a89b34..dac3257c0f 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -357,4 +357,16 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana refreshStatus() } + fun abortMerge(onSuccess: (() -> Unit)? = null) { + viewModelScope.launch { + try { + currentRepository?.abortMerge() + refreshStatus() + onSuccess?.invoke() + } catch (e: Exception) { + log.error("Failed to abort merge", e) + } + } + } + } diff --git a/app/src/main/res/layout/fragment_git_bottom_sheet.xml b/app/src/main/res/layout/fragment_git_bottom_sheet.xml index dda3b4bbad..8eff81abb2 100644 --- a/app/src/main/res/layout/fragment_git_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_git_bottom_sheet.xml @@ -140,7 +140,7 @@ android:layout_marginBottom="8dp" android:enabled="false" android:text="@string/commit" - app:layout_constraintBottom_toTopOf="@id/commitHistoryButton" /> + app:layout_constraintBottom_toTopOf="@id/btnAbortMerge" /> + + Authentication failed. Ensure your Personal Access Token has \'repo\' permissions, or try clearing and re-entering credentials. Pull completed with conflicts!\nPlease select the conflicted files from the list, resolve the highlighted issues in the editor, save file(s) and then commit your changes. Merge Conflicts + Abort Merge + Are you sure you want to abort the current merge? All conflict resolutions will be discarded. From bf4a48c0ded2591a403b3cf7011bde2f9d274da0 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 10 Apr 2026 15:43:35 +0100 Subject: [PATCH 7/8] featADFA-2885): Refresh files if merge is aborted --- .../activities/editor/EditorHandlerActivity.kt | 12 +++++++----- .../fragments/git/GitBottomSheetFragment.kt | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index 6a5327a761..08b24a3b1d 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -279,21 +279,23 @@ open class EditorHandlerActivity : * [onPause] snapshot **and** the in-memory buffer is still clean ([CodeEditorView.isModified] is * false). A clean buffer may still have undo history after [IDEEditor.markUnmodified] / save; we * reload anyway so external edits are not ignored. Never replaces buffers with unsaved edits. + * + * @param force If true, reloads even if the buffer is modified or the timestamp hasn't changed. */ - fun checkForExternalFileChanges() { + fun checkForExternalFileChanges(force: Boolean = false) { val openFiles = editorViewModel.getOpenedFiles() - if (openFiles.isEmpty() || fileTimestamps.isEmpty()) return + if (openFiles.isEmpty() || (fileTimestamps.isEmpty() && !force)) return lifecycleScope.launch(Dispatchers.IO) { openFiles.forEach { file -> - val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach + val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: 0L val currentTimestamp = file.lastModified() - if (currentTimestamp > lastKnownTimestamp) { + if (currentTimestamp > lastKnownTimestamp || force) { val newContent = runCatching { file.readText() }.getOrNull() ?: return@forEach withContext(Dispatchers.Main) { val editorView = getEditorForFile(file) ?: return@withContext - if (editorView.isModified) return@withContext + if (editorView.isModified && !force) return@withContext val ideEditor = editorView.editor ?: return@withContext ideEditor.setText(newContent) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index ee4a8086f3..f89dddbcd2 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -170,7 +170,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { viewModel.abortMerge { val activity = requireActivity() if (activity is EditorHandlerActivity) { - activity.checkForExternalFileChanges() + activity.checkForExternalFileChanges(force = true) } } } From 0bcc3e5aef804d810ea7f76628e60777ca60e1ec Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 10 Apr 2026 16:25:05 +0100 Subject: [PATCH 8/8] fix(ADFA-2885): Clean up merge state after aborting --- .../fragments/git/GitBottomSheetFragment.kt | 4 ++-- .../com/itsaky/androidide/git/core/JGitRepository.kt | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index f89dddbcd2..c8d7058607 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -58,7 +58,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { viewLifecycleOwner.lifecycleScope.launch { val repo = viewModel.currentRepository repo?.let { - activity.checkForExternalFileChanges() + activity.checkForExternalFileChanges(force = true) activity.openFile(File(repo.rootDir, change.path)) bottomSheetViewModel.setSheetState(BottomSheetBehavior.STATE_COLLAPSED) } @@ -163,7 +163,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { binding.commitDescription.doAfterTextChanged { validateCommitButton() } binding.btnAbortMerge.setOnClickListener { - val dialog = MaterialAlertDialogBuilder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.abort_merge) .setMessage(R.string.confirm_abort_merge) .setPositiveButton(R.string.abort_merge) { _, _ -> diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt index 008baaf57e..f95ddc47c3 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt @@ -282,8 +282,16 @@ class JGitRepository(override val rootDir: File) : GitRepository { } override suspend fun abortMerge(): Unit = withContext(Dispatchers.IO) { - if (repository.repositoryState == RepositoryState.MERGING) { - git.reset().setMode(ResetType.HARD).call() + // Reset working tree and index to HEAD + git.reset().setMode(ResetType.HARD).setRef(Constants.ORIG_HEAD).call() + + // Explicitly clear merge-related files to exit the MERGING state + repository.apply { + writeMergeHeads(null) + writeMergeCommitMsg(null) + writeCherryPickHead(null) + writeRevertHead(null) + writeSquashCommitMsg(null) } }