agent: Clean up git exclusions after emergency (#38775)

Oleksiy Syvokon created

In some rare cases, the auto-generated block gets stuck in
`.git/info/exclude`. We now auto-clean it.

Closes #38374

Release Notes:

- Remove auto-generated block from git excludes if it gets stuck there.

Change summary

crates/git/src/repository.rs | 46 +++++++++++++++++++++++++++++++++++--
1 file changed, 43 insertions(+), 3 deletions(-)

Detailed changes

crates/git/src/repository.rs 🔗

@@ -242,8 +242,20 @@ pub struct GitExcludeOverride {
 }
 
 impl GitExcludeOverride {
+    const START_BLOCK_MARKER: &str = "\n\n#  ====== Auto-added by Zed: =======\n";
+    const END_BLOCK_MARKER: &str = "\n#  ====== End of auto-added by Zed =======\n";
+
     pub async fn new(git_exclude_path: PathBuf) -> Result<Self> {
-        let original_excludes = smol::fs::read_to_string(&git_exclude_path).await.ok();
+        let original_excludes =
+            smol::fs::read_to_string(&git_exclude_path)
+                .await
+                .ok()
+                .map(|content| {
+                    // Auto-generated lines are normally cleaned up in
+                    // `restore_original()` or `drop()`, but may stuck in rare cases.
+                    // Make sure to remove them.
+                    Self::remove_auto_generated_block(&content)
+                });
 
         Ok(GitExcludeOverride {
             git_exclude_path,
@@ -260,9 +272,10 @@ impl GitExcludeOverride {
         });
 
         let mut content = self.original_excludes.clone().unwrap_or_default();
-        content.push_str("\n\n#  ====== Auto-added by Zed: =======\n");
+
+        content.push_str(Self::START_BLOCK_MARKER);
         content.push_str(self.added_excludes.as_ref().unwrap());
-        content.push('\n');
+        content.push_str(Self::END_BLOCK_MARKER);
 
         smol::fs::write(&self.git_exclude_path, content).await?;
         Ok(())
@@ -279,6 +292,33 @@ impl GitExcludeOverride {
 
         Ok(())
     }
+
+    fn remove_auto_generated_block(content: &str) -> String {
+        let start_marker = Self::START_BLOCK_MARKER;
+        let end_marker = Self::END_BLOCK_MARKER;
+        let mut content = content.to_string();
+
+        let start_index = content.find(start_marker);
+        let end_index = content.rfind(end_marker);
+
+        if let (Some(start), Some(end)) = (start_index, end_index) {
+            if end > start {
+                content.replace_range(start..end + end_marker.len(), "");
+            }
+        }
+
+        // Older versions of Zed didn't have end-of-block markers,
+        // so it's impossible to determine auto-generated lines.
+        // Conservatively remove the standard list of excludes
+        let standard_excludes = format!(
+            "{}{}",
+            Self::START_BLOCK_MARKER,
+            include_str!("./checkpoint.gitignore")
+        );
+        content = content.replace(&standard_excludes, "");
+
+        content
+    }
 }
 
 impl Drop for GitExcludeOverride {