Skip to content
This repository has been archived by the owner on Sep 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #8 from cosmonaut-nz/cli
Browse files Browse the repository at this point in the history
Feat: Added CLI fallback if no env variable is present for sensitive config
  • Loading branch information
avastmick authored Nov 28, 2023
2 parents 1c0ebde + 1cae3dd commit 06454d1
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 78 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
license = "MIT"
readme = "README.md"
authors = ["Mick Clarke <mick.clarke@cosmonaut.co.nz>"]
repository = "https://github.com/cosmonaut-nz/cosmonaut-code"
homepage = ""
description = """
Expand All @@ -23,6 +24,7 @@ image = "0.24.7"
log = "0.4.20"
env_logger = "0.10.1"
dotenvy = "0.15.7"
inquire = "0.6.2"
tokio = { version = "1.34.0", features = ["full"] }
reqwest = { version = "0.11.22", features = ["json"] }
config = "0.13.1"
Expand Down
26 changes: 20 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,42 @@ mod common;
mod provider;
mod review;
mod settings;
use log::{debug, error, info};
use log::{error, info};
use std::time::{Duration, Instant};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let start = Instant::now();
env_logger::init();

// Load settings
let settings = match settings::Settings::new() {
let settings: settings::Settings = match settings::Settings::new() {
Ok(cfg) => cfg,
Err(e) => {
error!("Failed to load settings: {}", e);
std::process::exit(1); // Exit if settings cannot be loaded
// Cannot recover due to incomplete configuration
std::process::exit(1);
}
};
debug!("SETTINGS LOADED: {:?}", settings);

// TODO: Wire up CLI here.

// Call the assess_codebase, according to user configuration, either from commandline, or json settings files.
review::assess_codebase(settings).await?;

info!("CODE REVIEW COMPLETE. See the output report for details.");

print_exec_duration(start.elapsed());
Ok(())
}

/// prints the execution time for the application at info log level
fn print_exec_duration(duration: Duration) {
let duration_secs = duration.as_secs();
let minutes = duration_secs / 60;
let seconds = duration_secs % 60;
let millis = duration.subsec_millis();

info!(
"TOTAL EXECUTION TIME: {} minutes, {} seconds, and {} milliseconds",
minutes, seconds, millis
);
}
1 change: 0 additions & 1 deletion src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ fn create_api_provider(
"openai" => Ok(Box::new(OpenAIProvider {
model: provider_settings.model.clone(),
})),
// Throw an error
_ => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Unsupported provider: {}", provider_settings.name),
Expand Down
33 changes: 28 additions & 5 deletions src/provider/prompts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A set of prompts for a chat-based LLM.
//!
//! Eventually, this is the location to 'tune' the prompts using something like 'Step-Back' or similar
//! This is the location to 'tune' the prompts using something like 'Step-Back' or similar
//! The prompt can be specific to a provider
//!
//!

Expand All @@ -12,9 +13,10 @@ use serde::{Deserialize, Serialize};
const FILE_REVIEW_SCHEMA: &str = include_str!("../provider/specification/file_review.schema.json");
const JSON_HANDLING_ADVICE: &str = r#"Provide your analysis in valid JSON format.
Strictly escape any characters within your response strings that will create invalid JSON, such as \" - i.e., double quotes.
Never use comments in your JSON. Ensure that your output exactly complies to the following JSON Schema
Never use comments in your JSON. Ensure that your output exactly conforms to the following JSON Schema
and you follow the instructions provided in "description" fields."#;

/// Holds the id and [`Vec`] of [`ProviderCompletionMessage`]s
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct PromptData {
pub(crate) id: Option<String>,
Expand Down Expand Up @@ -84,7 +86,28 @@ impl PromptData {
],
}
}
/// gets a [`PromptData`] for a LLM to summarise the README in a repository for the RepositoryReview.repository_purpose field
// TODO: the overall summary of
pub(crate) fn _get_readme_summary_prompt(for_provider: &ProviderSettings) -> Self {
debug!("Provider: {}", for_provider);
Self {
id: None,
messages: vec![ProviderCompletionMessage {
role: ProviderMessageRole::System,
content: "PROMPT TO WRITE HERE".to_string(),
}],
}
}
/// gets a [`PromptData`] for a LLM to summarise the overall review from a [`Vec`] of [`FileReview`]
// TODO: the overall summary of the returned FileReviews for the RepositoryReview.summary field
pub(crate) fn _get_overall_summary_prompt(for_provider: &ProviderSettings) -> Self {
debug!("Provider: {}", for_provider);
Self {
id: None,
messages: vec![ProviderCompletionMessage {
role: ProviderMessageRole::System,
content: "PROMPT TO WRITE HERE".to_string(),
}],
}
}
}

// TODO Add in prompts to summarise a file - e.g., the list of FileReview summaries after the code is reviewed, and the repository README.md
//
2 changes: 0 additions & 2 deletions src/review/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
use crate::review::data::LanguageFileType;
use linguist::{
container::InMemoryLanguageContainer,
// error::LinguistError,
resolver::{resolve_language_from_content_str, Language, Scope},
// utils::{is_configuration, is_documentation, is_dotfile, is_vendor},
};
use log::error;
use regex::RegexSet;
Expand Down
88 changes: 54 additions & 34 deletions src/review/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ use crate::review::data::{FileReview, LanguageFileType, RAGStatus, RepositoryRev
use crate::review::tools::{get_git_contributors, is_not_blacklisted};
use crate::settings::Settings;
use chrono::{DateTime, Local, Utc};
use log::{debug, error, info};
use log::{debug, error, info, warn};
use regex::Regex;
use serde::Deserialize;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use walkdir::{DirEntry, WalkDir};

Expand All @@ -35,30 +34,21 @@ pub(crate) async fn assess_codebase(
settings: Settings,
) -> Result<RepositoryReview, Box<dyn std::error::Error>> {
// Used for the final report to write to disk
let output_dir = PathBuf::from(&settings.report_output_path);
let output_file_path =
let output_dir: PathBuf = PathBuf::from(&settings.report_output_path);
let output_file_path: PathBuf =
create_timestamped_filename(&output_dir, &settings.output_type, Local::now());
// Collect the review data in the following data struct
let mut review = RepositoryReview::new();
match extract_directory_name(&settings.repository_path) {
let mut review: RepositoryReview = RepositoryReview::new();

match extract_repository_name(&settings.repository_path) {
Ok(dir_name) => review.repository_name(dir_name.to_string()),
Err(e) => {
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)));
}
Err(e) => return Err(Box::new(e)),
};
let repository_root = match validate_repository(Path::new(&settings.repository_path)) {
Ok(path) => path,
Err(e) => return Err(Box::new(e)),
};
let repository_root = Path::new(&settings.repository_path);
if !repository_root.is_dir() {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Provided path is not a directory: {}",
&settings.repository_path
),
)));
}
// Get the repository blacklist to avoid unneccessary file/folder traversal (should be largely derived from .gitignore)
let blacklisted_dirs: Vec<String> = tools::get_blacklist_dirs(repository_root);
debug!("BLACKLIST: {:?}", blacklisted_dirs);

let mut overall_file_count: i32 = 0;
let (lc, mut breakdown, rules, docs) = initialize_language_analysis();
Expand Down Expand Up @@ -90,23 +80,30 @@ pub(crate) async fn assess_codebase(
file_info.file_size.unwrap(),
file_info.loc.unwrap(),
);
// To improve development feedback loop time on big repos, allows sampling
#[cfg(debug_assertions)]
if let Some(max_count) = settings.max_file_count {
if overall_file_count >= max_count {
if overall_file_count > max_count {
continue;
}
}
let contents_str = match file_info.contents.to_str() {
Some(contents) => contents,
None => {
error!("Contents of the code file are not valid UTF-8");
error!(
"Contents of the code file, {:?}, are not valid UTF-8, skipping.",
entry.file_name()
);
continue;
}
};
let file_name_str = match file_info.name.to_str() {
Some(name) => name,
None => {
error!("File name is not valid UTF-8");
error!(
"File name, {:?}, is not valid UTF-8, skipping.",
entry.file_name()
);
continue;
}
};
Expand All @@ -120,7 +117,9 @@ pub(crate) async fn assess_codebase(
Ok(Some(reviewed_file)) => {
review.add_file_review(reviewed_file);
}
Ok(None) => {} // Handle cases where no review is needed
Ok(None) => {
warn!("No review actioned. None returned from 'review_file'")
}
Err(e) => {
return Err(e);
}
Expand All @@ -130,7 +129,6 @@ pub(crate) async fn assess_codebase(
let now_utc: DateTime<Utc> = Utc::now();
let now_local = now_utc.with_timezone(&Local);
let review_date = now_local.format("%H:%M, %d/%m/%Y").to_string();
debug!("Review date: {}", review_date);

// Complete the fields in the [`RepositoryReview`] struct
if let Some(language) =
Expand Down Expand Up @@ -167,10 +165,32 @@ pub(crate) async fn assess_codebase(
.write_all(review_json.as_bytes())
.map_err(|e| format!("Error writing to output file: {}", e))?;

info!("Total number of files processed: {}", overall_file_count);
info!("TOTAL NUMBER OF FILES PROCESSED: {}", overall_file_count);
Ok(review)
}

/// validates the provided [`Path`] as being a directory that holds a '.git' subdirectory - i.e. is a valid git repository
fn validate_repository(repository_root: &Path) -> Result<&Path, PathError> {
if !repository_root.is_dir() {
return Err(PathError {
message: format!(
"Provided path is not a directory: {}",
repository_root.display()
),
});
}
if !repository_root.join(".git").is_dir() {
return Err(PathError {
message: format!(
"Provided path is not a valid Git repository: {}",
repository_root.display()
),
});
}

Ok(repository_root)
}

/// gets the content, filename and extension of a [`walkdir::DirEntry`]
fn get_file_info(entry: &DirEntry) -> Option<FileInfo> {
let path = entry.path();
Expand Down Expand Up @@ -251,7 +271,7 @@ impl ReviewType {
/// # Parameters
///
/// * `Settings` - A [`Settings`] that contains information for the LLM
/// * `path` - - The path the the file to process
/// * `path` - The path the the file to process
///
async fn review_file(
settings: &Settings,
Expand Down Expand Up @@ -341,7 +361,7 @@ impl fmt::Display for PathError {
}
impl Error for PathError {}

fn extract_directory_name(path_str: &str) -> Result<&str, PathError> {
fn extract_repository_name(path_str: &str) -> Result<&str, PathError> {
let path = Path::new(path_str);

// Check if the path points to a file (has an extension)
Expand Down Expand Up @@ -475,24 +495,24 @@ mod tests {
#[test]
fn test_normal_directory_path() {
let path_str = "/location/dirname/cosmonaut-code";
assert_eq!(extract_directory_name(path_str).unwrap(), "cosmonaut-code");
assert_eq!(extract_repository_name(path_str).unwrap(), "cosmonaut-code");
}

#[test]
fn test_empty_path() {
let path_str = "";
assert!(extract_directory_name(path_str).is_err());
assert!(extract_repository_name(path_str).is_err());
}

#[test]
fn test_path_ending_with_slash() {
let path_str = "/location/dirname/cosmonaut-code/";
assert_eq!(extract_directory_name(path_str).unwrap(), "cosmonaut-code");
assert_eq!(extract_repository_name(path_str).unwrap(), "cosmonaut-code");
}

#[test]
fn test_single_name_directory() {
let path_str = "cosmonaut-code";
assert_eq!(extract_directory_name(path_str).unwrap(), "cosmonaut-code");
assert_eq!(extract_repository_name(path_str).unwrap(), "cosmonaut-code");
}
}
Loading

0 comments on commit 06454d1

Please sign in to comment.