1use clap::{Parser, Subcommand};
2use log::info;
3use std::collections::HashMap;
4use std::fs::File;
5use std::io::{BufRead, BufReader, Read};
6use std::path::Path;
7use std::{env, path::PathBuf};
8
9#[derive(Debug)]
10pub enum Error {
11 MissingHeadLog,
12 MalformedCheckoutLog,
13 NoCurrentBranch,
14 IOError(std::io::Error),
15}
16
17impl From<std::io::Error> for Error {
18 fn from(err: std::io::Error) -> Self {
19 Error::IOError { 0: err }
20 }
21}
22
23type BranchHistory = HashMap<String, u32>;
24
25#[derive(Parser)]
26#[clap(name = "git-ctx")]
27#[clap(about="git context switching", long_about=None)]
28pub struct Cli {
29 #[clap(subcommand)]
30 pub command: Commands,
31}
32
33#[derive(Subcommand)]
34pub enum Commands {
35 #[clap(alias = "l")]
36 ListBranches {
37 #[clap(short, default_value = "10")]
38 limit: usize,
39 },
40
41 #[clap(alias = "s")]
42 SwitchBranch {
43 #[clap(short, default_value = "10")]
44 limit: usize,
45 },
46}
47
48#[derive(Debug)]
49pub struct Git {
50 git_folder: PathBuf,
51}
52
53impl Git {
54 fn find_first_path_with_git_folder(folder: Option<PathBuf>) -> Option<PathBuf> {
55 if let Some(folder) = folder {
56 let git_path = format!("{}/.git", folder.to_str().unwrap());
57 let p = Path::new(&git_path).to_owned();
58 if p.exists() {
59 return Some(p);
60 }
61
62 if let Some(parent) = folder.parent() {
63 return Git::find_first_path_with_git_folder(Some(parent.to_owned()));
64 }
65 }
66 None
67 }
68
69 pub fn new() -> Self {
70 let cwd = env::current_dir().unwrap();
71 let git_folder =
72 Git::find_first_path_with_git_folder(Some(cwd)).expect("unable to find .git folder");
73 Git { git_folder }
74 }
75
76 pub fn get_current_branch(&mut self) -> Result<String, Error> {
77 let f = self.git_folder.join("HEAD");
78
79 let head_file = File::open(f)?;
80 let mut buf_reader = BufReader::new(head_file);
81 let mut content = String::new();
82 buf_reader.read_to_string(&mut content)?;
83 match content.trim().split("/").last() {
84 Some(s) => Ok(String::from(s)),
85 None => Err(Error::NoCurrentBranch),
86 }
87 }
88
89 fn parse_head_log(&mut self) -> Result<BranchHistory, Error> {
90 let mut ret: BranchHistory = BranchHistory::new();
91 let f = self.git_folder.join("logs/HEAD");
92
93 if !f.exists() {
94 return Err(Error::MissingHeadLog);
95 }
96
97 let head_log_file = File::open(f)?;
98 let mut buf_reader = BufReader::new(head_log_file);
99 let mut seq = 0;
100
101 loop {
102 let mut line = String::new();
103 let bytes_read = buf_reader.read_line(&mut line)?;
104 if bytes_read == 0 {
105 break;
106 }
107 info!("line={}", line);
108 match line.trim().split('\t').nth(1) {
109 Some(msg) => {
110 if !msg.starts_with("checkout: moving from ") {
111 continue;
112 }
113
114 let msg = &msg["checkout: moving from ".len()..msg.len()];
115
116 let parts: Vec<&str> = msg.split(" to ").collect();
117 if parts.len() != 2 {
118 return Err(Error::MalformedCheckoutLog);
119 }
120
121 ret.insert(String::from(parts[0]), seq);
122 ret.insert(String::from(parts[1]), seq + 1);
123 seq += 1;
124 }
125 None => continue,
126 }
127 }
128
129 Ok(ret)
130 }
131
132 pub fn get_recent_branches(&mut self, limit: usize) -> Result<Vec<String>, Error> {
133 let branch_history = self.parse_head_log()?;
134
135 let mut items_vec: Vec<(&String, &u32)> = branch_history.iter().collect();
136 items_vec.sort_by_key(|k| k.1);
137 items_vec.reverse();
138
139 let branches = items_vec
140 .into_iter()
141 .map(|(branch, _seq)| branch.clone())
142 .take(limit)
143 .collect();
144
145 Ok(branches)
146 }
147}