git_ctx/
lib.rs

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}