commit_info/lib.rs
1#![warn(missing_docs)]
2#![warn(rustdoc::missing_doc_code_examples)]
3
4//! This crate gathers relevant git info from any Repo. Some of the info returned includes:
5//! - **Git status info**: Checks if a repo is dirty, has been modified and so on.
6//! - **Commits**: Gathers and shows information for the last 10 commits
7//!
8//! ## Example
9//! ```rust
10//! # let mut path = env::current_dir().unwrap();
11//! # path.push("test_project");
12//! # let dir = path.to_string_lossy().to_string();
13//! // let dir = "/path/to/repo"; <- Point to the location of t=your repo
14//! let info = Info::new(&dir).status_info()?.commit_info()?;
15//! println("{:#?}", info);
16
17// Copyright 2022 Anthony Mugendi
18//
19// Licensed under the Apache License, Version 2.0 (the "License");
20// you may not use this file except in compliance with the License.
21// You may obtain a copy of the License at
22//
23// http://www.apache.org/licenses/LICENSE-2.0
24//
25// Unless required by applicable law or agreed to in writing, software
26// distributed under the License is distributed on an "AS IS" BASIS,
27// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28// See the License for the specific language governing permissions and
29// limitations under the License.
30
31use anyhow::Result;
32use chrono::{DateTime, Utc};
33use cmd_lib::run_fun;
34use serde::{Deserialize, Serialize};
35use serde_json::{from_str, json};
36use std::{collections::HashMap, path::PathBuf};
37
38/// The Status Struct:
39/// Holds information about the status of the repo
40#[derive(Debug, Clone)]
41pub struct Status {
42 /// Holds any error thrown by ```git status```
43 pub error: Option<String>,
44 /// Indicates if repo is dirty or not. For this, we check both ```git status -s``` and ```git diff -stat```
45 pub git_dirty: Option<bool>,
46 /// A HashMap describing the state of the repo
47 pub summary: HashMap<String, bool>,
48}
49
50/// Struct holding info of each commit
51#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
52pub struct Commit {
53 /// The repo commit date
54 #[serde(with = "my_date_format")]
55 pub commit_date: Option<DateTime<Utc>>,
56 /// The repo commit message
57 pub commit_message: Option<String>,
58 /// The repo author name
59 pub author_name: Option<String>,
60 /// The repo author email
61 pub author_email: Option<String>,
62 /// The repo committer name (sometimes the author is not always the committer)
63 pub committer_name: Option<String>,
64 /// The repo committer email
65 pub committer_email: Option<String>,
66 /// tree hash
67 pub tree_hash: Option<String>,
68}
69
70/// The main struct that returns combined Status and Commits info
71#[derive(Debug, Clone)]
72pub struct Info {
73 /// Repo directory
74 pub dir: String,
75 /// Boolean indicating id the directory above is indeed a repo
76 pub is_git: bool,
77 /// Repo branch inspected
78 pub branch: Option<String>,
79 /// Status information for the repo
80 pub status: Option<Status>,
81 /// Information on the repo commits
82 pub commits: Option<Vec<Commit>>,
83}
84
85
86impl Commit {
87 /// To initialize a blank Commit Struct
88 pub fn new() -> Commit {
89 Commit {
90 // branch: "".into(),
91 commit_date: None,
92 commit_message: None,
93 author_name: None,
94 author_email: None,
95 committer_name: None,
96 committer_email: None,
97 tree_hash: None,
98 }
99 }
100}
101
102impl Info {
103 /// To initialize the Info Struct. A &str pointing to the repo directory is expected
104 /// This implementation method checks that the directory does indeed exist and that the repo is a git repo
105 /// It returns a new Info Struct with the "dir" and "is_git" fields set
106 ///
107 /// ## Example
108 /// ```
109 /// # let mut path = env::current_dir().unwrap();
110 /// # path.push("test_project");
111 /// # let dir = path.to_string_lossy().to_string();
112 /// // let dir = "/path/to/repo"; <- Point to the location of t=your repo
113 /// let info = Info::new(&dir);
114 /// println("{:#?}", info);
115 /// ```
116 pub fn new(dir: &str) -> Info {
117 // check if dir is_git
118 let mut project_path = PathBuf::from(dir);
119 project_path.push(".git");
120
121 let is_git = project_path.exists();
122
123 Info {
124 dir: dir.into(),
125 is_git: is_git,
126 status: None,
127 commits: None,
128 branch: None,
129 }
130 }
131
132 /// Get information of all the commits.
133 /// This Method returns Info in its result.
134 /// If there are no commits, the returned value is None
135 /// ## Example
136 /// ```
137 /// # let mut path = env::current_dir().unwrap();
138 /// # path.push("test_project");
139 /// # let dir = path.to_string_lossy().to_string();
140 /// // let dir = "/path/to/repo"; <- Point to the location of t=your repo
141 /// let commits_info = Info::new(&dir).commit_info()?;
142 /// println("{:#?}", commits_info);
143 /// ```
144 pub fn commit_info(&self) -> Result<Info> {
145 let mut git_info = self.clone();
146
147 if git_info.is_git {
148 let dir = &git_info.dir;
149
150 //check diff
151 let branch = match run_fun!(
152 cd ${dir};
153 git branch -r | grep -v HEAD | head -n 1 ;
154 ) {
155 Ok(resp) => {
156 let r = resp.clone();
157 r
158 }
159 _ => "".into(),
160 };
161
162 let branch = &branch[..];
163 let branch = branch.trim();
164 // println!("BBB >> {:?}", branch);
165 git_info.branch = Some(branch.into());
166
167 let format = format!("{{\"commit_date\":\"%ci\", \"commit_message\":\"%s\", \"author_name\":\"%an\", \"author_email\":\"%ae\", \"committer_name\":\"%cn\", \"committer_email\":\"%ce\", \"tree_hash\":\"%t\"}}");
168
169 // let format = "%ci";
170
171 let empty_commit = json!(Commit::new());
172
173 let commits = match run_fun!(
174 cd ${dir};
175 git log --format="$format" $branch
176 // git status
177 ) {
178 Ok(resp) => resp,
179 Err(_) => {
180 // println!("{:#?}", e);
181 // Commit::new()
182 empty_commit.to_string()
183 }
184 };
185
186 // println!("{:#?}", commits);
187
188 let commits = commits.split("\n").collect::<Vec<&str>>();
189 let len: usize = if commits.len() > 5 { 5 } else { commits.len() };
190
191 // pick top
192 let top_commits: Vec<Commit> = commits[0..len]
193 .to_vec()
194 .iter()
195 .map(|s| {
196 let commit: Commit = match from_str(s) {
197 Ok(c) => c,
198 _ => Commit::new(),
199 };
200 commit
201 })
202 .filter(|e: &Commit| {
203 // let b:&Commit = e;
204 e.commit_date != None
205 })
206 .collect();
207
208 git_info.commits = if top_commits.len() > 0 {
209 Some(top_commits)
210 } else {
211 None
212 };
213
214 // println!("{:#?}",);
215 // git_info
216 }
217 Ok(git_info)
218 }
219
220 /// This method returns status information for the repo
221 /// ## Example
222 /// ```
223 /// # let mut path = env::current_dir().unwrap();
224 /// # path.push("test_project");
225 /// # let dir = path.to_string_lossy().to_string();
226 /// // let dir = "/path/to/repo"; <- Point to the location of t=your repo
227 /// let status_info = Info::new(&dir).status_info()?;
228 /// println("{:#?}", status_info);
229 /// ```
230 pub fn status_info(&self) -> Result<Info> {
231 let mut git_info = self.clone();
232 let mut status = Status {
233 error: None,
234 git_dirty: None,
235 summary: HashMap::new(),
236 };
237
238 if git_info.is_git {
239 let dir = &git_info.dir;
240
241 match run_fun!( cd ${dir}; git status -s; ) {
242 // if we can run git status then it is a git directory
243 Ok(resp) => {
244 //
245 let is_modified = resp.len() > 0;
246
247 //check diff
248 let resp = match run_fun!( cd ${dir}; git diff --stat; ) {
249 Ok(r) => r,
250 _ => "ERR".into(),
251 };
252 let is_dirty = resp.len() > 0;
253
254 status.summary.insert("is_modified".into(), is_modified);
255 status.summary.insert("is_dirty".into(), is_dirty);
256 status.git_dirty = Some(is_dirty || is_modified);
257 }
258 Err(e) => {
259 status.error = Some(format!("{:?}", e));
260 }
261 };
262 }
263
264 git_info.status = Some(status);
265
266 Ok(git_info)
267 }
268}
269
270mod my_date_format {
271 use chrono::{DateTime, TimeZone, Utc};
272 use serde::{self, Deserialize, Deserializer, Serializer};
273
274 // 2014-08-29 16:09:40 -0600
275
276 const FORMAT: &'static str = "%Y-%m-%d %H:%M:%S %Z";
277
278 // The signature of a serialize_with function must follow the pattern:
279 //
280 // fn serialize<S>(&T, S) -> Result<S::Ok, S::Error>
281 // where
282 // S: Serializer
283 //
284 // although it may also be generic over the input types T.
285 pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
286 where
287 S: Serializer,
288 {
289 let s = match date {
290 Some(dt) => {
291 let s = format!("{}", dt.format(FORMAT));
292 s
293 }
294 _ => "null".into(),
295 };
296
297 serializer.serialize_str(&s)
298 }
299
300 // The signature of a deserialize_with function must follow the pattern:
301 //
302 // fn deserialize<'de, D>(D) -> Result<T, D::Error>
303 // where
304 // D: Deserializer<'de>
305 //
306 // although it may also be generic over the output types T.
307 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
308 where
309 D: Deserializer<'de>,
310 {
311 let s = String::deserialize(deserializer)?;
312
313 let dt = Utc
314 .datetime_from_str(&s, FORMAT)
315 .map_err(serde::de::Error::custom)?;
316
317 Ok(Some(dt))
318 }
319}
320
321
322// To successfully run tests, first create a "test_project" directory at the home of this crate
323// Do so by running cargo new test_project
324// It is not included so you will need to create it yourself
325#[cfg(test)]
326mod tests {
327
328 use super::Info;
329 use std::env;
330
331 fn test_dir() -> String {
332 let mut path = env::current_dir().unwrap();
333 path.push("test_project");
334
335 path.to_string_lossy().to_string()
336 }
337
338 #[test]
339 fn it_works() {
340 let dir = test_dir();
341
342 let info = Info::new(&dir)
343 .status_info()
344 .expect("Unable to get status info")
345 .commit_info()
346 .expect("Unable to get commit info");
347
348 assert_eq!(None, info.commits);
349 assert_eq!(Some(true), info.status.expect("err").git_dirty);
350 }
351}