Skip to content

ADFA-3074: Clone error handling#1064

Merged
dara-abijo-adfa merged 11 commits intostagefrom
ADFA-3074-clone-error-handling
Mar 20, 2026
Merged

ADFA-3074: Clone error handling#1064
dara-abijo-adfa merged 11 commits intostagefrom
ADFA-3074-clone-error-handling

Conversation

@dara-abijo-adfa
Copy link
Contributor

@dara-abijo-adfa dara-abijo-adfa commented Mar 11, 2026

  • Show Retry button when Git clone fails
  • Make error messages more descriptive
  • Add Cancel clone button

@coderabbitai

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt (1)

165-182: ⚠️ Potential issue | 🟡 Minor

Missing retryButton visibility handling in Success state.

The retryButton visibility is set in Idle, Cloning, and Error states but not in Success. While the Success state quickly transitions away, explicitly hiding it ensures consistent UI state.

🛡️ Suggested fix
                            is CloneRepoUiState.Success -> {
                                cloneButton.isEnabled = true
+                               retryButton.visibility = View.GONE
                                statusText.text = getString(R.string.clone_successful)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt`
around lines 165 - 182, In the CloneRepositoryFragment's handling of is
CloneRepoUiState.Success, explicitly hide the retryButton (e.g., set its
visibility to GONE or isVisible = false) before or when you reset the UI and
call viewModel.resetState so the button state stays consistent with
Idle/Cloning/Error branches; update the Success branch where cloneButton,
statusText, destDir check, and viewModel.resetState() are handled to include
hiding retryButton.
🧹 Nitpick comments (4)
resources/src/main/res/values/strings.xml (1)

1170-1177: Minor casing inconsistency in desc_file_conflicted.

The string "Conflicted File" uses title case, while all other desc_file_* strings use sentence case (e.g., "File added", "File modified"). Consider aligning the format for consistency.

💅 Suggested fix
-    <string name="desc_file_conflicted">Conflicted File</string>
+    <string name="desc_file_conflicted">File conflicted</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/src/main/res/values/strings.xml` around lines 1170 - 1177, The
string resource desc_file_conflicted is inconsistent (uses "Conflicted File"
title case) — update its value to match the other desc_file_* entries by using
the same sentence/phrase form; specifically change the value for string
name="desc_file_conflicted" to "File conflicted" so it aligns with "File added",
"File modified", etc.
app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt (1)

163-165: String-based error detection is fragile but pragmatic.

The message matching for "Unexpected end of stream" and "Software caused connection abort" may differ across JGit versions and platforms. However, this is already preceded by an EOFException type check. If targeting more robustness, consider traversing the exception cause chain to check for SocketException or other IOException subtypes before falling back to message matching. Note that similar string-based matching patterns exist elsewhere in the codebase (e.g., WebServer.kt).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`
around lines 163 - 165, Replace the fragile message-based detection in the
isConnectionDrop logic by walking the exception cause chain (starting from the
caught exception e) and checking for specific IO-related types like
EOFException, SocketException or other IOException subclasses before falling
back to string matching; update the check that currently uses e.cause and
e.message to iterate through causes and return true on any matching exception
type (refer to isConnectionDrop and the caught exception variable e) so the code
is robust across JGit versions/platforms while retaining the existing message
checks as a last resort.
app/src/main/res/layout/fragment_clone_repository.xml (2)

112-146: Minor: Inconsistent ID naming convention.

The button IDs mix naming styles:

  • exit_button (snake_case)
  • retryButton (camelCase)
  • cloneButton (camelCase)

While View Binding handles exit_button correctly (converts to exitButton), consistent naming improves maintainability. Consider standardizing on camelCase (exitButton) to match the others.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/layout/fragment_clone_repository.xml` around lines 112 -
146, Rename the inconsistent id exit_button to camelCase exitButton in the
layout and update all usages in code to match (replace R.id.exit_button or any
findViewById/reference and any viewBinding usages) so that ids for retryButton,
cloneButton, and exitButton use the same naming convention; after changing the
id, rebuild/run to fix any lingering references or generated binding names.

61-68: Consider using MaterialCheckBox for theme consistency.

The layout uses Material3 styles throughout (MaterialTextView, MaterialButton, Material TextInputLayout), but this CheckBox is the standard Android widget. Using com.google.android.material.checkbox.MaterialCheckBox would provide consistent theming and ripple behavior.

♻️ Suggested change
-        <CheckBox
+        <com.google.android.material.checkbox.MaterialCheckBox
             android:id="@+id/authCheckbox"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="16dp"
             android:text="@string/use_authentication"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/localPathLayout" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/layout/fragment_clone_repository.xml` around lines 61 - 68,
Replace the standard CheckBox in fragment_clone_repository.xml (id authCheckbox)
with the Material component to match the rest of your Material3 widgets: change
the XML tag to com.google.android.material.checkbox.MaterialCheckBox, keep the
existing attributes (android:id="@+id/authCheckbox", android:layout_width,
android:layout_height, android:layout_marginTop, android:text and constraint
attributes) and ensure your app uses the Material components theme and the
Material library dependency so the control inherits ripples and theming like
MaterialTextView/MaterialButton.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt`:
- Around line 165-182: In the CloneRepositoryFragment's handling of is
CloneRepoUiState.Success, explicitly hide the retryButton (e.g., set its
visibility to GONE or isVisible = false) before or when you reset the UI and
call viewModel.resetState so the button state stays consistent with
Idle/Cloning/Error branches; update the Success branch where cloneButton,
statusText, destDir check, and viewModel.resetState() are handled to include
hiding retryButton.

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`:
- Around line 163-165: Replace the fragile message-based detection in the
isConnectionDrop logic by walking the exception cause chain (starting from the
caught exception e) and checking for specific IO-related types like
EOFException, SocketException or other IOException subclasses before falling
back to string matching; update the check that currently uses e.cause and
e.message to iterate through causes and return true on any matching exception
type (refer to isConnectionDrop and the caught exception variable e) so the code
is robust across JGit versions/platforms while retaining the existing message
checks as a last resort.

In `@app/src/main/res/layout/fragment_clone_repository.xml`:
- Around line 112-146: Rename the inconsistent id exit_button to camelCase
exitButton in the layout and update all usages in code to match (replace
R.id.exit_button or any findViewById/reference and any viewBinding usages) so
that ids for retryButton, cloneButton, and exitButton use the same naming
convention; after changing the id, rebuild/run to fix any lingering references
or generated binding names.
- Around line 61-68: Replace the standard CheckBox in
fragment_clone_repository.xml (id authCheckbox) with the Material component to
match the rest of your Material3 widgets: change the XML tag to
com.google.android.material.checkbox.MaterialCheckBox, keep the existing
attributes (android:id="@+id/authCheckbox", android:layout_width,
android:layout_height, android:layout_marginTop, android:text and constraint
attributes) and ensure your app uses the Material components theme and the
Material library dependency so the control inherits ripples and theming like
MaterialTextView/MaterialButton.

In `@resources/src/main/res/values/strings.xml`:
- Around line 1170-1177: The string resource desc_file_conflicted is
inconsistent (uses "Conflicted File" title case) — update its value to match the
other desc_file_* entries by using the same sentence/phrase form; specifically
change the value for string name="desc_file_conflicted" to "File conflicted" so
it aligns with "File added", "File modified", etc.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dcbb7850-fdea-4c82-8c29-611e9d07ba24

📥 Commits

Reviewing files that changed from the base of the PR and between 99c02e8 and 99aa492.

📒 Files selected for processing (5)
  • app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt
  • app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt
  • app/src/main/res/layout/fragment_clone_repository.xml
  • app/src/main/res/values/strings.xml
  • resources/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
  • app/src/main/res/values/strings.xml

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt (1)

178-191: Inspect the full cause chain before falling back to generic clone errors.

Checking only e.cause and a couple of English message fragments is brittle. JGit/network failures are often nested more deeply, so the same connectivity issue can still land in the generic clone_failed path. A small helper that walks the cause chain will make the new error mapping much more reliable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`
around lines 178 - 191, The current connectivity checks in
CloneRepositoryViewModel (the isNetworkError/isConnectionDrop logic and
subsequent errorResId/errorMessage) only inspect e.cause and top-level message
fragments, which misses nested causes; implement a helper (e.g., a private
function that walks the cause chain for a Throwable) and use it to detect
UnknownHostException, EOFException, or any message containing "Unexpected end of
stream" or "Software caused connection abort" anywhere in the cause chain, then
replace the existing isNetworkError/isConnectionDrop checks to call that helper
so errorResId is set reliably before falling back to the generic clone_failed
path.
app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt (1)

150-150: isCancellable isn't driving the UI yet.

The new CloneRepoUiState.Cloning.isCancellable bit is ignored here, so the cancel button is shown for every cloning state regardless of what the ViewModel reports. Either gate visibility/enabled state on state.isCancellable and populate it, or remove the field to avoid dead state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt`
at line 150, The cancel button visibility currently ignores the new
Cloning.isCancellable flag; update the UI in CloneRepositoryFragment (where
cancelButton.visibility is set) to only show/enable the button when state is
CloneRepoUiState.Cloning AND state.isCancellable is true, and ensure the
ViewModel populates CloneRepoUiState.Cloning.isCancellable appropriately during
cloning (or remove the unused isCancellable field if you prefer not to use it)
so the UI reflects the real cancellability state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt`:
- Around line 169-173: The UI currently flips the CTA to "Retry" for every
CloneRepoUiState.Error in CloneRepositoryFragment by calling
cloneButton.refreshStatus(isForRetry = true) inside the is
CloneRepoUiState.Error branch; instead, extend CloneRepoUiState.Error with a
boolean (e.g., retryable) or add a distinct error subtype to indicate whether
the error came from a failed clone attempt versus a local validation issue (like
destination_directory_not_empty), then only call
cloneButton.refreshStatus(isForRetry = true) when that retryable flag is true
(otherwise call refreshStatus with isForRetry = false or another appropriate
state) so the CTA text/icon is only switched for actual retryable clone
failures.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`:
- Line 30: Replace the plain Boolean isCloneCancelled with a thread-safe flag
(e.g., an AtomicBoolean or a `@Volatile` var) so ProgressMonitor callbacks on the
IO dispatcher observe cancellations reliably, and change updateProgressUI() to
update the state atomically by reading and transforming the current _uiState
(e.g., use _uiState.update { current -> if (current is
CloneRepositoryUiState.Cloning) current.copy(progress = progress) else current }
or a compare-and-set style) so a stale Cloning snapshot cannot overwrite a newer
Error or Idle state; refer to the isCloneCancelled symbol, updateProgressUI(),
and the _uiState / CloneRepositoryUiState.Cloning types to locate where to apply
these changes.

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt`:
- Line 150: The cancel button visibility currently ignores the new
Cloning.isCancellable flag; update the UI in CloneRepositoryFragment (where
cancelButton.visibility is set) to only show/enable the button when state is
CloneRepoUiState.Cloning AND state.isCancellable is true, and ensure the
ViewModel populates CloneRepoUiState.Cloning.isCancellable appropriately during
cloning (or remove the unused isCancellable field if you prefer not to use it)
so the UI reflects the real cancellability state.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`:
- Around line 178-191: The current connectivity checks in
CloneRepositoryViewModel (the isNetworkError/isConnectionDrop logic and
subsequent errorResId/errorMessage) only inspect e.cause and top-level message
fragments, which misses nested causes; implement a helper (e.g., a private
function that walks the cause chain for a Throwable) and use it to detect
UnknownHostException, EOFException, or any message containing "Unexpected end of
stream" or "Software caused connection abort" anywhere in the cause chain, then
replace the existing isNetworkError/isConnectionDrop checks to call that helper
so errorResId is set reliably before falling back to the generic clone_failed
path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44dd6ba6-d0ea-40d6-bf43-eaf1a4331258

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca6917 and ac88546.

📒 Files selected for processing (5)
  • app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt
  • app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt
  • app/src/main/res/layout/fragment_clone_repository.xml
  • git-core/src/main/java/com/itsaky/androidide/git/core/models/CloneRepoUiState.kt
  • resources/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • resources/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/res/layout/fragment_clone_repository.xml

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt (1)

162-169: 🛠️ Refactor suggestion | 🟠 Major

Catch specific JGit exceptions instead of all exceptions.

The catch block at line 169 catches Exception generically, masking unexpected runtime bugs and converting them into retryable clone errors. This contradicts the codebase pattern (e.g., JGitRepository.getHistory() catches NoHeadException narrowly). Additionally, the postcondition check at line 162 should not throw a generic Exception. Identify the specific JGit exceptions from cloneRepository() (likely TransportException and IO-related exceptions) and catch only those; use check() or require() for the postcondition instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`
around lines 162 - 169, Replace the generic postcondition throw and broad catch
with narrow checks and exception handlers: change the postcondition that
currently does throw Exception("Destination directory was not created.") to use
check(localPath.exists() && localPath.isDirectory) or require(...) so it fails
with an IllegalStateException if the directory wasn't created; then replace
catch (e: Exception) with catches for the specific JGit and IO exceptions that
clone can throw (e.g., catch TransportException,
InvalidRemoteException/GitAPIException, and java.io.IOException) and handle each
by updating _uiState to the appropriate error state (keeping hasCloned = true
and CloneRepoUiState.Success(localPath) logic intact). Ensure you import the
specific exception classes from JGit and remove the generic Exception catch so
unrelated runtime errors are not swallowed.
♻️ Duplicate comments (1)
app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt (1)

30-30: ⚠️ Potential issue | 🔴 Critical

Use a thread-safe cancellation flag and state-based update.

isCloneCancelled is written from cloneRepository()/cancelClone() and read from ProgressMonitor.isCancelled() across threads. The plain Boolean can miss cancellation, and updateProgressUI() closes over a stale _uiState.value, so a late progress tick can overwrite a newer Idle/Error state back to Cloning.

🔧 Minimal fix
-    private var isCloneCancelled = false
+    `@Volatile`
+    private var isCloneCancelled = false
@@
-                        val currentState = _uiState.value
-                        if (currentState is CloneRepoUiState.Cloning) {
-                            _uiState.update {
-                                currentState.copy(
+                        _uiState.update { currentState ->
+                            if (currentState is CloneRepoUiState.Cloning) {
+                                currentState.copy(
                                     cloneProgress = progressMsg,
                                     clonePercentage = percentage,
                                     isCancellable = true,
                                     statusTextResId = R.string.cloning_repo
                                 )
+                            } else {
+                                currentState
                             }
-                        }
+                        }

Also applies to: 63-63, 126-128, 145-154, 232-234

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`
at line 30, Replace the plain Boolean cancellation flag with a thread-safe
AtomicBoolean (e.g., isCloneCancelled = AtomicBoolean(false)) and read it from
ProgressMonitor.isCancelled() via isCloneCancelled.get(); set it via
isCloneCancelled.set(true) in cancelClone() and reset it at the start of
cloneRepository(). Replace any direct writes that close over _uiState.value in
updateProgressUI() with a state-based update using _uiState.update { current ->
if (current is Cloning) current.withUpdatedProgress(newProgress) else current }
(or equivalent when/branching) so a late progress tick never overwrites
Idle/Error states; also only transition to the Cloning state when starting the
operation after clearing the cancel flag. Ensure all references
(isCloneCancelled, cloneRepository(), cancelClone(),
ProgressMonitor.isCancelled(), updateProgressUI(), _uiState.value) are updated
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`:
- Around line 162-169: Replace the generic postcondition throw and broad catch
with narrow checks and exception handlers: change the postcondition that
currently does throw Exception("Destination directory was not created.") to use
check(localPath.exists() && localPath.isDirectory) or require(...) so it fails
with an IllegalStateException if the directory wasn't created; then replace
catch (e: Exception) with catches for the specific JGit and IO exceptions that
clone can throw (e.g., catch TransportException,
InvalidRemoteException/GitAPIException, and java.io.IOException) and handle each
by updating _uiState to the appropriate error state (keeping hasCloned = true
and CloneRepoUiState.Success(localPath) logic intact). Ensure you import the
specific exception classes from JGit and remove the generic Exception catch so
unrelated runtime errors are not swallowed.

---

Duplicate comments:
In
`@app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt`:
- Line 30: Replace the plain Boolean cancellation flag with a thread-safe
AtomicBoolean (e.g., isCloneCancelled = AtomicBoolean(false)) and read it from
ProgressMonitor.isCancelled() via isCloneCancelled.get(); set it via
isCloneCancelled.set(true) in cancelClone() and reset it at the start of
cloneRepository(). Replace any direct writes that close over _uiState.value in
updateProgressUI() with a state-based update using _uiState.update { current ->
if (current is Cloning) current.withUpdatedProgress(newProgress) else current }
(or equivalent when/branching) so a late progress tick never overwrites
Idle/Error states; also only transition to the Cloning state when starting the
operation after clearing the cancel flag. Ensure all references
(isCloneCancelled, cloneRepository(), cancelClone(),
ProgressMonitor.isCancelled(), updateProgressUI(), _uiState.value) are updated
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73f9f227-833b-454d-8f0c-88b378e79e08

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1eaaa and da1bbcf.

📒 Files selected for processing (4)
  • app/src/main/java/com/itsaky/androidide/fragments/CloneRepositoryFragment.kt
  • app/src/main/java/com/itsaky/androidide/viewmodel/CloneRepositoryViewModel.kt
  • git-core/src/main/java/com/itsaky/androidide/git/core/models/CloneRepoUiState.kt
  • resources/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • resources/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • git-core/src/main/java/com/itsaky/androidide/git/core/models/CloneRepoUiState.kt

@dara-abijo-adfa dara-abijo-adfa merged commit 0a1d98c into stage Mar 20, 2026
2 checks passed
@dara-abijo-adfa dara-abijo-adfa deleted the ADFA-3074-clone-error-handling branch March 20, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants