1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6use error_stack::ResultExt;
7use git2::Repository;
8
9use crate::{
10 configs::Config,
11 dirty_paths::DirtyUtf8Path,
12 error::TmsError,
13 repos::{find_repos, find_submodules},
14 tmux::Tmux,
15 Result,
16};
17
18pub struct Session {
19 pub name: String,
20 pub session_type: SessionType,
21}
22
23pub enum SessionType {
24 Git(Repository),
25 Bookmark(PathBuf),
26}
27
28impl Session {
29 pub fn new(name: String, session_type: SessionType) -> Self {
30 Session { name, session_type }
31 }
32
33 pub fn path(&self) -> &Path {
34 match &self.session_type {
35 SessionType::Git(repo) if repo.is_bare() => repo.path(),
36 SessionType::Git(repo) => repo.path().parent().unwrap(),
37 SessionType::Bookmark(path) => path,
38 }
39 }
40
41 pub fn switch_to(&self, tmux: &Tmux, config: &Config) -> Result<()> {
42 match &self.session_type {
43 SessionType::Git(repo) => self.switch_to_repo_session(repo, tmux, config),
44 SessionType::Bookmark(path) => self.switch_to_bookmark_session(tmux, path, config),
45 }
46 }
47
48 fn switch_to_repo_session(
49 &self,
50 repo: &Repository,
51 tmux: &Tmux,
52 config: &Config,
53 ) -> Result<()> {
54 let path = if repo.is_bare() {
55 repo.path().to_path_buf().to_string()?
56 } else {
57 repo.workdir()
58 .expect("bare repositories should all have parent directories")
59 .canonicalize()
60 .change_context(TmsError::IoError)?
61 .to_string()?
62 };
63 let session_name = self.name.replace('.', "_");
64
65 if !tmux.session_exists(&session_name) {
66 tmux.new_session(Some(&session_name), Some(&path));
67 tmux.set_up_tmux_env(repo, &session_name)?;
68 tmux.run_session_create_script(self.path(), &session_name, config)?;
69 }
70
71 tmux.switch_to_session(&session_name);
72
73 Ok(())
74 }
75
76 fn switch_to_bookmark_session(&self, tmux: &Tmux, path: &Path, config: &Config) -> Result<()> {
77 let session_name = self.name.replace('.', "_");
78
79 if !tmux.session_exists(&session_name) {
80 tmux.new_session(Some(&session_name), path.to_str());
81 tmux.run_session_create_script(path, &session_name, config)?;
82 }
83
84 tmux.switch_to_session(&session_name);
85
86 Ok(())
87 }
88}
89
90pub trait SessionContainer {
91 fn find_session(&self, name: &str) -> Option<&Session>;
92 fn insert_session(&mut self, name: String, repo: Session);
93 fn list(&self) -> Vec<String>;
94}
95
96impl SessionContainer for HashMap<String, Session> {
97 fn find_session(&self, name: &str) -> Option<&Session> {
98 self.get(name)
99 }
100
101 fn insert_session(&mut self, name: String, session: Session) {
102 self.insert(name, session);
103 }
104
105 fn list(&self) -> Vec<String> {
106 let mut list: Vec<String> = self.keys().map(|s| s.to_owned()).collect();
107 list.sort();
108
109 list
110 }
111}
112
113pub fn create_sessions(config: &Config) -> Result<impl SessionContainer> {
114 let mut sessions = find_repos(config)?;
115 sessions = append_bookmarks(config, sessions)?;
116
117 let sessions = generate_session_container(sessions, config)?;
118
119 Ok(sessions)
120}
121
122fn generate_session_container(
123 mut sessions: HashMap<String, Vec<Session>>,
124 config: &Config,
125) -> Result<impl SessionContainer> {
126 let mut ret = HashMap::new();
127
128 for list in sessions.values_mut() {
129 if list.len() == 1 {
130 let session = list.pop().unwrap();
131 insert_session(&mut ret, session, config)?;
132 } else {
133 let deduplicated = deduplicate_sessions(list);
134
135 for session in deduplicated {
136 insert_session(&mut ret, session, config)?;
137 }
138 }
139 }
140
141 Ok(ret)
142}
143
144fn insert_session(
145 sessions: &mut impl SessionContainer,
146 session: Session,
147 config: &Config,
148) -> Result<()> {
149 let visible_name = if config.display_full_path == Some(true) {
150 session.path().display().to_string()
151 } else {
152 session.name.clone()
153 };
154 if let SessionType::Git(repo) = &session.session_type {
155 if config.search_submodules == Some(true) {
156 if let Ok(submodules) = repo.submodules() {
157 find_submodules(submodules, &visible_name, sessions, config)?;
158 }
159 }
160 }
161 sessions.insert_session(visible_name, session);
162 Ok(())
163}
164
165fn deduplicate_sessions(duplicate_sessions: &mut Vec<Session>) -> Vec<Session> {
166 let mut depth = 1;
167 let mut deduplicated = Vec::new();
168 while let Some(current_session) = duplicate_sessions.pop() {
169 let mut equal = true;
170 let current_path = current_session.path();
171 let mut current_depth = 1;
172
173 while equal {
174 equal = false;
175 if let Some(current_str) = current_path.iter().rev().nth(current_depth) {
176 for session in &mut *duplicate_sessions {
177 if let Some(str) = session.path().iter().rev().nth(current_depth) {
178 if str == current_str {
179 current_depth += 1;
180 equal = true;
181 break;
182 }
183 }
184 }
185 }
186 }
187
188 deduplicated.push(current_session);
189 depth = depth.max(current_depth);
190 }
191
192 for session in &mut deduplicated {
193 session.name = {
194 let mut count = depth + 1;
195 let mut iterator = session.path().iter().rev();
196 let mut str = String::new();
197
198 while count > 0 {
199 if let Some(dir) = iterator.next() {
200 if str.is_empty() {
201 str = dir.to_string_lossy().to_string();
202 } else {
203 str = format!("{}/{}", dir.to_string_lossy(), str);
204 }
205 count -= 1;
206 } else {
207 count = 0;
208 }
209 }
210
211 str
212 };
213 }
214
215 deduplicated
216}
217
218fn append_bookmarks(
219 config: &Config,
220 mut sessions: HashMap<String, Vec<Session>>,
221) -> Result<HashMap<String, Vec<Session>>> {
222 let bookmarks = config.bookmark_paths();
223
224 for path in bookmarks {
225 let session_name = path
226 .file_name()
227 .expect("The file name doesn't end in `..`")
228 .to_string()?;
229 let session = Session::new(session_name, SessionType::Bookmark(path));
230 if let Some(list) = sessions.get_mut(&session.name) {
231 list.push(session);
232 } else {
233 sessions.insert(session.name.clone(), vec![session]);
234 }
235 }
236
237 Ok(sessions)
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn verify_session_name_deduplication() {
246 let mut test_sessions = vec![
247 Session::new(
248 "test".into(),
249 SessionType::Bookmark("/search/path/to/proj1/test".into()),
250 ),
251 Session::new(
252 "test".into(),
253 SessionType::Bookmark("/search/path/to/proj2/test".into()),
254 ),
255 Session::new(
256 "test".into(),
257 SessionType::Bookmark("/other/path/to/projects/proj2/test".into()),
258 ),
259 ];
260
261 let deduplicated = deduplicate_sessions(&mut test_sessions);
262
263 assert_eq!(deduplicated[0].name, "projects/proj2/test");
264 assert_eq!(deduplicated[1].name, "to/proj2/test");
265 assert_eq!(deduplicated[2].name, "to/proj1/test");
266 }
267}