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			InputBox:None,
116		};
117
118		self.ApplicationState
119			.Feature
120			.Markers
121			.SourceControlManagementProviders
122			.lock()
123			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
124			.insert(Handle, ProviderState.clone());
125
126		self.ApplicationState
127			.Feature
128			.Markers
129			.SourceControlManagementGroups
130			.lock()
131			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
132			.insert(Handle, Default::default());
133
134		self.ApplicationHandle
135			.emit(SkyEvent::SCMProviderAdded.AsStr(), ProviderState)
136			.map_err(|Error| {
137				CommonError::UserInterfaceInteraction { Reason:format!("Failed to emit scm event: {}", Error) }
138			})?;
139
140		Ok(Handle)
141	}
142
143	async fn DisposeSourceControl(&self, ProviderHandle:u32) -> Result<(), CommonError> {
144		dev_log!(
145			"extensions",
146			"[SourceControlManagementProvider] Disposing SCM provider with handle {}",
147			ProviderHandle
148		);
149
150		self.ApplicationState
151			.Feature
152			.Markers
153			.SourceControlManagementProviders
154			.lock()
155			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
156			.remove(&ProviderHandle);
157
158		self.ApplicationState
159			.Feature
160			.Markers
161			.SourceControlManagementGroups
162			.lock()
163			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?
164			.remove(&ProviderHandle);
165
166		self.ApplicationHandle
167			.emit(SkyEvent::SCMProviderRemoved.AsStr(), ProviderHandle)
168			.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
169
170		Ok(())
171	}
172
173	async fn UpdateSourceControl(&self, ProviderHandle:u32, UpdateDataValue:Value) -> Result<(), CommonError> {
174		let UpdateData:SourceControlUpdateDTO = serde_json::from_value(UpdateDataValue)?;
175
176		dev_log!(
177			"extensions",
178			"[SourceControlManagementProvider] Updating provider {}",
179			ProviderHandle
180		);
181
182		let mut ProvidersGuard = self
183			.ApplicationState
184			.Feature
185			.Markers
186			.SourceControlManagementProviders
187			.lock()
188			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
189
190		if let Some(Provider) = ProvidersGuard.get_mut(&ProviderHandle) {
191			if let Some(count) = UpdateData.Count {
192				Provider.Count = Some(count);
193			}
194
195			if let Some(value) = UpdateData.InputBoxValue {
196				if let Some(input_box) = &mut Provider.InputBox {
197					input_box.Value = value;
198				}
199			}
200
201			let ProviderClone = Provider.clone();
202
203			// Release lock before emitting
204			drop(ProvidersGuard);
205
206			self.ApplicationHandle
207				.emit(
208					SkyEvent::SCMProviderChanged.AsStr(),
209					json!({ "handle": ProviderHandle, "provider": ProviderClone }),
210				)
211				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
212		}
213
214		Ok(())
215	}
216
217	async fn UpdateSourceControlGroup(&self, ProviderHandle:u32, GroupDataValue:Value) -> Result<(), CommonError> {
218		let GroupData:SourceControlGroupUpdateDTO = serde_json::from_value(GroupDataValue)?;
219
220		dev_log!(
221			"extensions",
222			"[SourceControlManagementProvider] Updating group '{}' for provider {}",
223			GroupData.GroupID,
224			ProviderHandle
225		);
226
227		let mut GroupsGuard = self
228			.ApplicationState
229			.Feature
230			.Markers
231			.SourceControlManagementGroups
232			.lock()
233			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
234
235		if let Some(ProviderGroups) = GroupsGuard.get_mut(&ProviderHandle) {
236			let Group = ProviderGroups.entry(GroupData.GroupID.clone()).or_insert_with(|| {
237				SourceControlManagementGroupDTO {
238					ProviderHandle,
239					Identifier:GroupData.GroupID.clone(),
240					Label:GroupData.Label.clone(),
241				}
242			});
243
244			Group.Label = GroupData.Label;
245
246			let GroupClone = Group.clone();
247
248			// Release lock before emitting
249			drop(GroupsGuard);
250
251			self.ApplicationHandle
252				.emit(
253					SkyEvent::SCMGroupChanged.AsStr(),
254					json!({ "providerHandle": ProviderHandle, "group": GroupClone }),
255				)
256				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
257		} else {
258			dev_log!(
259				"extensions",
260				"warn: [SourceControlManagementProvider] Received group update for unknown provider handle: {}",
261				ProviderHandle
262			);
263		}
264
265		Ok(())
266	}
267
268	async fn RegisterInputBox(&self, ProviderHandle:u32, InputBoxDataValue:Value) -> Result<(), CommonError> {
269		let InputBoxData:SourceControlInputBoxDTO = serde_json::from_value(InputBoxDataValue)?;
270
271		dev_log!(
272			"extensions",
273			"[SourceControlManagementProvider] Registering input box for provider {}",
274			ProviderHandle
275		);
276
277		let mut ProvidersGuard = self
278			.ApplicationState
279			.Feature
280			.Markers
281			.SourceControlManagementProviders
282			.lock()
283			.map_err(Utility::ErrorMapping::MapApplicationStateLockErrorToCommonError)?;
284
285		if let Some(Provider) = ProvidersGuard.get_mut(&ProviderHandle) {
286			Provider.InputBox = Some(InputBoxData);
287
288			let ProviderClone = Provider.clone();
289
290			// Release lock before emitting
291			drop(ProvidersGuard);
292
293			self.ApplicationHandle
294				.emit(
295					SkyEvent::SCMProviderChanged.AsStr(),
296					json!({ "handle": ProviderHandle, "provider": ProviderClone }),
297				)
298				.map_err(|Error| CommonError::UserInterfaceInteraction { Reason:Error.to_string() })?;
299		}
300
301		Ok(())
302	}
303}