Skip to main content

Mountain/Environment/
SourceControlManagementProvider.rs

1//! # SourceControlManagementProvider (Environment)
2//!
3//! Implements the `SourceControlManagementProvider` trait for the
4//! `MountainEnvironment`.
5//!
6//! ## SCM provider architecture
7//!
8//! Each SCM provider maintains:
9//! - **Handle** - unique `u32` identifier; callers may supply their own so the
10//!   same handle key used in `ScmNamespace.ts` maps correctly on both sides of
11//!   the IPC boundary.
12//! - **Label** - user-friendly name (e.g., "Git")
13//! - **Root URI** - URI of the repository root
14//! - **Groups** - resource groups organizing changed resources
15//! - **Input box** - user input widget (e.g., commit messages)
16//! - **Count** - badge count for changed items
17//!
18//! ## Resource groups
19//!
20//! Groups organize resources by their state:
21//! - **Changes** - modified files ready to commit
22//! - **Untracked** - new files not yet tracked
23//! - **Staged** - files staged for commit
24//! - **Merge changes** - files with merge conflicts
25//! - **Conflict unresolved** - unresolved conflict markers
26//!
27//! ## SCM lifecycle
28//!
29//! 1. **CreateSourceControl** - register provider, emit `SCMProviderAdded`
30//! 2. **UpdateSourceControl** - update badge/input-box, emit
31//!    `SCMProviderChanged`
32//! 3. **UpdateSourceControlGroup** - upsert group entry, emit `SCMGroupChanged`
33//! 4. **RegisterInputBox** - attach input-box DTO to provider
34//! 5. **DisposeSourceControl** - remove provider + groups, emit
35//!    `SCMProviderRemoved`
36//!
37//! ## Git integration patterns
38//!
39//! Typical Git provider workflow:
40//! - Detect `.git` directory, run `git status` to populate groups
41//! - Run `git diff` for file diffs; use input box for commit messages
42//! - Show badge count for changed files
43//! - Provide commands: Stage, Unstage, Commit, Push, Pull, Discard
44//!
45//! ## VS Code reference
46//!
47//! - `vs/workbench/services/scm/common/scmService.ts`
48//! - `vs/platform/scm/common/scm.ts`
49//! - `vs/sourcecontrol/git/common/git.ts`
50
51use CommonLibrary::{
52	Error::CommonError::CommonError,
53	IPC::SkyEvent::SkyEvent,
54	SourceControlManagement::{
55		DTO::{
56			SourceControlCreateDTO::SourceControlCreateDTO,
57			SourceControlGroupUpdateDTO::SourceControlGroupUpdateDTO,
58			SourceControlInputBoxDTO::SourceControlInputBoxDTO,
59			SourceControlManagementGroupDTO::SourceControlManagementGroupDTO,
60			SourceControlManagementProviderDTO::SourceControlManagementProviderDTO,
61			SourceControlUpdateDTO::SourceControlUpdateDTO,
62		},
63		SourceControlManagementProvider::SourceControlManagementProvider,
64	},
65};
66use async_trait::async_trait;
67use serde_json::{Value, json};
68use tauri::Emitter;
69
70use super::{MountainEnvironment::MountainEnvironment, Utility};
71use crate::dev_log;
72
73// TODO: built-in Git provider (libgit2 or CLI), repository discovery +
74// change detection, staging/unstaging/committing, branch management UI,
75// remote ops (push/pull/fetch), merge-conflict UI, Git LFS + submodules,
76// credential management, SCM extensions API, history/blame views, stash/pop,
77// tag management, detached HEAD / bisect, rebase / cherry-pick, telemetry.
78#[async_trait]
79impl SourceControlManagementProvider for MountainEnvironment {
80	async fn CreateSourceControl(&self, ProviderDataValue:Value) -> Result<u32, CommonError> {
81		let ProviderData:SourceControlCreateDTO = serde_json::from_value(ProviderDataValue)?;
82
83		// Honor caller-supplied handle when present so the marker maps
84		// (`SourceControlManagementProviders` / `SourceControlManagementGroups`)
85		// key under the SAME identifier Cocoon's `ScmNamespace.ts` uses
86		// for subsequent `register_scm_resource_group` and `update_scm_group`
87		// notifications. Without this, `UpdateSourceControlGroup` looks up
88		// Cocoon's handle in a map keyed by a Mountain-allocated handle,
89		// the entry isn't there, and every group update warns
90		// "Received group update for unknown provider handle: <H>" while
91		// the SCM viewlet stays empty.
92		let Handle = ProviderData
93			.Handle
94			.unwrap_or_else(|| self.ApplicationState.GetNextSourceControlManagementProviderHandle());
95
96		dev_log!(
97			"extensions",
98			"[SourceControlManagementProvider] Creating new SCM provider with handle {}",
99			Handle
100		);
101
102		let ProviderState = SourceControlManagementProviderDTO {
103			Handle,
104
105			Identifier:ProviderData.ID.clone(),
106
107			Label:ProviderData.Label,
108
109			RootURI:Some(json!({ "external": ProviderData.RootUri.to_string() })),
110
111			CommitTemplate:None,
112
113			Count:None,
114
115			AcceptInputCommand:None,
116
117			InputBox:None,
118		};
119
120		self.ApplicationState
121			.Feature
122			.Markers
123			.SourceControlManagementProviders
124			.lock()
125			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
126			.insert(Handle, ProviderState.clone());
127
128		self.ApplicationState
129			.Feature
130			.Markers
131			.SourceControlManagementGroups
132			.lock()
133			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
134			.insert(Handle, Default::default());
135
136		self.ApplicationHandle
137			.emit(SkyEvent::SCMProviderAdded.AsStr(), ProviderState)
138			.map_err(|Error| {
139				CommonError::UserInterfaceInteraction { Reason:format!("Failed to emit scm event: {}", Error) }
140			})?;
141
142		Ok(Handle)
143	}
144
145	async fn DisposeSourceControl(&self, ProviderHandle:u32) -> Result<(), CommonError> {
146		dev_log!(
147			"extensions",
148			"[SourceControlManagementProvider] Disposing SCM provider with handle {}",
149			ProviderHandle
150		);
151
152		self.ApplicationState
153			.Feature
154			.Markers
155			.SourceControlManagementProviders
156			.lock()
157			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
158			.remove(&ProviderHandle);
159
160		self.ApplicationState
161			.Feature
162			.Markers
163			.SourceControlManagementGroups
164			.lock()
165			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
166			.remove(&ProviderHandle);
167
168		self.ApplicationHandle
169			.emit(SkyEvent::SCMProviderRemoved.AsStr(), ProviderHandle)
170			.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
171
172		Ok(())
173	}
174
175	async fn UpdateSourceControl(&self, ProviderHandle:u32, UpdateDataValue:Value) -> Result<(), CommonError> {
176		let UpdateData:SourceControlUpdateDTO = serde_json::from_value(UpdateDataValue)?;
177
178		dev_log!(
179			"extensions",
180			"[SourceControlManagementProvider] Updating provider {}",
181			ProviderHandle
182		);
183
184		let mut ProvidersGuard = self
185			.ApplicationState
186			.Feature
187			.Markers
188			.SourceControlManagementProviders
189			.lock()
190			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
191
192		if let Some(Provider) = ProvidersGuard.get_mut(&ProviderHandle) {
193			if let Some(count) = UpdateData.Count {
194				Provider.Count = Some(count);
195			}
196
197			if let Some(value) = UpdateData.InputBoxValue {
198				if let Some(input_box) = &mut Provider.InputBox {
199					input_box.Value = value;
200				}
201			}
202
203			if let Some(placeholder) = UpdateData.InputBoxPlaceholder {
204				if let Some(input_box) = &mut Provider.InputBox {
205					input_box.Placeholder = Some(placeholder);
206				}
207			}
208
209			if let Some(template) = UpdateData.CommitTemplate {
210				Provider.CommitTemplate = Some(template);
211			}
212
213			if let Some(cmd) = UpdateData.AcceptInputCommand {
214				Provider.AcceptInputCommand = Some(cmd);
215			}
216
217			let ProviderClone = Provider.clone();
218
219			// Release lock before emitting
220			drop(ProvidersGuard);
221
222			self.ApplicationHandle
223				.emit(
224					SkyEvent::SCMProviderChanged.AsStr(),
225					json!({ "handle": ProviderHandle, "provider": ProviderClone }),
226				)
227				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
228		}
229
230		Ok(())
231	}
232
233	async fn UpdateSourceControlGroup(&self, ProviderHandle:u32, GroupDataValue:Value) -> Result<(), CommonError> {
234		let GroupData:SourceControlGroupUpdateDTO = serde_json::from_value(GroupDataValue)?;
235
236		dev_log!(
237			"extensions",
238			"[SourceControlManagementProvider] Updating group '{}' for provider {}",
239			GroupData.GroupID,
240			ProviderHandle
241		);
242
243		let mut GroupsGuard = self
244			.ApplicationState
245			.Feature
246			.Markers
247			.SourceControlManagementGroups
248			.lock()
249			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
250
251		if let Some(ProviderGroups) = GroupsGuard.get_mut(&ProviderHandle) {
252			let Group = ProviderGroups.entry(GroupData.GroupID.clone()).or_insert_with(|| {
253				SourceControlManagementGroupDTO {
254					ProviderHandle,
255					Identifier:GroupData.GroupID.clone(),
256					Label:GroupData.Label.clone(),
257				}
258			});
259
260			Group.Label = GroupData.Label;
261
262			let GroupClone = Group.clone();
263
264			// Release lock before emitting
265			drop(GroupsGuard);
266
267			self.ApplicationHandle
268				.emit(
269					SkyEvent::SCMGroupChanged.AsStr(),
270					json!({ "providerHandle": ProviderHandle, "group": GroupClone }),
271				)
272				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
273		} else {
274			dev_log!(
275				"extensions",
276				"warn: [SourceControlManagementProvider] Received group update for unknown provider handle: {}",
277				ProviderHandle
278			);
279		}
280
281		Ok(())
282	}
283
284	async fn RegisterInputBox(&self, ProviderHandle:u32, InputBoxDataValue:Value) -> Result<(), CommonError> {
285		let InputBoxData:SourceControlInputBoxDTO = serde_json::from_value(InputBoxDataValue)?;
286
287		dev_log!(
288			"extensions",
289			"[SourceControlManagementProvider] Registering input box for provider {}",
290			ProviderHandle
291		);
292
293		let mut ProvidersGuard = self
294			.ApplicationState
295			.Feature
296			.Markers
297			.SourceControlManagementProviders
298			.lock()
299			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
300
301		if let Some(Provider) = ProvidersGuard.get_mut(&ProviderHandle) {
302			Provider.InputBox = Some(InputBoxData);
303
304			let ProviderClone = Provider.clone();
305
306			// Release lock before emitting
307			drop(ProvidersGuard);
308
309			self.ApplicationHandle
310				.emit(
311					SkyEvent::SCMProviderChanged.AsStr(),
312					json!({ "handle": ProviderHandle, "provider": ProviderClone }),
313				)
314				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
315		}
316
317		Ok(())
318	}
319}