Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-2885: Handle merge conflicts#1167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
8f9bdad8d8d1c4578dbead97106cb0969483aafb64c44b4dc988b42ebf4a48c0bcc3e5f028a4459bbee5777acfe2f8ac48File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,24 +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 | ||
| @@ -43,9 +50,27 @@ 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.checkForExternalFileChanges(force = true) | ||
| 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() | ||
| @@ -81,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 | ||
| @@ -89,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 { | ||
| @@ -97,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) | ||
| } | ||
| @@ -134,6 +162,22 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { | ||
| binding.commitSummary.doAfterTextChanged { validateCommitButton() } | ||
| binding.commitDescription.doAfterTextChanged { validateCommitButton() } | ||
| binding.btnAbortMerge.setOnClickListener { | ||
| 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(force = true) | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| .setNegativeButton(android.R.string.cancel, null) | ||
| .show() | ||
| } | ||
| binding.authorAvatar.setOnClickListener { | ||
| showAuthorPopup() | ||
| } | ||
| @@ -214,25 +258,40 @@ 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() | ||
| refreshEditorContent() | ||
| } | ||
| 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() | ||
| refreshEditorContent() | ||
| } | ||
| 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) | ||
| @@ -278,6 +337,13 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { | ||
| .show() | ||
| } | ||
| private fun refreshEditorContent(force: Boolean = false) { | ||
| val activity = requireActivity() | ||
| if (activity is EditorHandlerActivity) { | ||
| activity.checkForExternalFileChanges(force) | ||
| } | ||
| } | ||
| override fun onDestroyView() { | ||
| super.onDestroyView() | ||
| _binding = null | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| @@ -219,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 | ||
| ) | ||
| } | ||
| @@ -256,6 +264,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 +286,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") | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| private fun handlePullSuccess( | ||
| @@ -302,7 +321,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<String>? = null) : PullUiState() | ||
| } | ||
| sealed class PushUiState { | ||
| @@ -337,4 +357,16 @@ class GitBottomSheetViewModel(private val credentialsManager: GitCredentialsMana | ||
| refreshStatus() | ||
| } | ||
| fun abortMerge(onSuccess: (() -> Unit)? = null) { | ||
| viewModelScope.launch { | ||
| try { | ||
| currentRepository?.abortMerge() | ||
| refreshStatus() | ||
| onSuccess?.invoke() | ||
dara-abijo-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } catch (e: Exception) { | ||
| log.error("Failed to abort merge", e) | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.