-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: git diff extraction and code refactor #8
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
wzslr321
changed the title
feat: add basic
feat: git diff extraction and code refactor
Oct 9, 2024
thiserror
support
Impactifier ReportTotal Changes: 1 file(s) changed. diff --git a/.github/workflows/impactifier.yml b/.github/workflows/impactifier.yml
index db2b6d9..d7a5877 100644
--- a/.github/workflows/impactifier.yml
+++ b/.github/workflows/impactifier.yml
@@ -11,17 +11,18 @@ permissions:
issues: write
pull-requests: write
jobs:
impactifier:
runs-on: ubuntu-latest
steps:
# 1. Checkout the repository with full history
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-depth: 0 # Fetch all history for all branches
# 2. Cache Cargo Registry
- name: Cache Cargo Registry
uses: actions/cache@v3
with:
@@ -30,6 +31,7 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-registry-
# 3. Cache Cargo Build
- name: Cache Cargo Build
uses: actions/cache@v3
with:
@@ -38,24 +40,98 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-build-
# 4. Build Impactifier
- name: Build Impactifier
run: |
cargo build --release --manifest-path Cargo.toml
# 5. Run Impactifier and generate diff.json
- name: Run Impactifier
id: run_impactifier
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./target/release/impactifier --config impactifier-config.yaml
./target/release/impactifier --tracing-level=0 --from-branch=main --to-branch=refactor
# 6. (Optional) Output diff.json for debugging
- name: Output diff.json (Debug)
if: ${{ github.event_name == 'pull_request' }}
run: |
cat diff.json
# 7. Post Comment on Pull Request with the diff
- name: Post Comment on Pull Request
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: \`## Impactifier Report\`
});
const fs = require('fs');
const path = 'diff.json'; // Path to the diff JSON file
// Check if the diff file exists
if (!fs.existsSync(path)) {
console.log('No diff.json file found.');
return;
}
// Read and parse the diff JSON
let diffData;
try {
const rawData = fs.readFileSync(path, 'utf8');
diffData = JSON.parse(rawData);
} catch (error) {
console.error('Failed to read or parse diff.json:', error);
return;
}
// Format the diff for the comment
let formattedDiff = '';
if (diffData.deltas && Array.isArray(diffData.deltas)) {
diffData.deltas.forEach(delta => {
if (delta.value) {
// Escape backticks in the delta.value to prevent breaking the Markdown
const safeValue = delta.value.replace(/\`/g, '\\\`');
formattedDiff += \`${safeValue}\n\`;
}
});
} else {
formattedDiff = 'No differences found.';
}
// Handle large diffs by truncating (optional)
const maxLength = 60000; // GitHub comment limit
let truncatedDiff = formattedDiff;
if (formattedDiff.length > maxLength) {
truncatedDiff = formattedDiff.substring(0, maxLength) + '\n... (diff truncated)';
}
// Create a summary based on the number of deltas
let summary = '';
if (diffData.deltas && diffData.deltas.length > 0) {
summary = \`**Total Changes:** ${diffData.deltas.length} file(s) changed.\n\n\`;
} else {
summary = 'No changes detected between the specified branches.\n\n';
}
// Create the comment body with summary and diff
const commentBody = \`## Impactifier Report
${summary}
\\`\\`\\`diff
${truncatedDiff}
\\`\\`\\`\`;
// Post the comment to the pull request
try {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
console.log('Impactifier report posted successfully.');
} catch (error) {
console.error('Failed to post Impactifier report:', error);
}
diff --git a/Cargo.lock b/Cargo.lock
index 0f157c5..c39b024 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,6 +66,12 @@ dependencies = [
]
[[package]]
name = "anyhow"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "async-trait"
version = "0.1.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -351,6 +357,7 @@ dependencies = [
name = "impactifier"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"config",
"git2",
@@ -358,7 +365,9 @@ dependencies = [
"rhai",
"serde",
"serde_derive",
"serde_json",
"serde_yaml",
"thiserror",
"toml",
"tracing",
"tracing-subscriber",
@@ -766,9 +775,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.125"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
@@ -872,18 +881,18 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b"
[[package]]
name = "thiserror"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index 91725db..a2845e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.89"
clap = { version = "4.5.16", features = ["derive"] }
config = "0.14.0"
git2 = "0.19.0"
@@ -13,7 +14,9 @@ lazy_static = "1.5.0"
rhai = "1.19.0"
serde = "1.0.208"
serde_derive = "1.0.208"
serde_json = "1.0.128"
serde_yaml = "0.9.34"
thiserror = "1.0.64"
toml = "0.8.19"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
diff --git a/README.md b/README.md
index 2be4dbf..70a25a9 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,25 @@
# Impactifier
Impactifier is a tool designed to analyze the impact of code changes, across the whole - possibly separated - codebase, and allow to take various actions based on that. It aims to help developers identify potential downstream effects of changes, allowing for more reliable and frequent releases. Can be used as either local tool or as part of CI/CD.
> This project is build in public, therefore I strive to stream its development on [Twitch](https://www.twitch.tv/creatixd)
<br>
---
<p align = "center">
<b> <i> Show your support by giving a :star: </b> </i>
</p>
---
<br>
Impactifier is a tool designed to analyze and visualize the impact of code changes, across the whole - possibly separated - codebase.
It helps developers identify potential downstream effects of changes, allowing for more reliable and frequent releases.
## Table of Contents
1. [Overview](#overview)
2. [Key Features](#key-features)
3. [Getting Started](#getting-started)
3. [Roadmap](#roadmap)
4. [Getting Started](#getting-started)
- [CI/CD](#cicd)
- [CLI](#cli)
- [Configuration File](#configuration-file)
4. [Contributing](#contributing)
5. [License](#license)
6. [Contributing](#contributing)
7. [License](#license)
## Overview
> *Please note, that a lot of this README was created with help of ChatGPT, so it is far from perfect*
Impactifier provides an automated approach to change impact analysis, designed to serve as both CI/CD and local tool.
Aims to improve frequency and reliability of releases, by generating impact reports for engineers to be able to push the changes with confidence. (Eventually) Highly configurable, with possibility to add custom rules and actions.
Your contracts generator does weird stuff under the hood, yet you want to see what impact on front-end modifying its query handler have? Don't worry, **Impactifier got your back.**
Impactifier provides an automated approach to change impact analysis, initally designed to serve as CI/CD tool.
Aims to improve frequency and reliability of releases, by generating impact reports for engineers to be able to push the changes with confidence.
(Eventually) Highly configurable, with possibility to add custom rules. Your contracts generator does weird stuff under the hood, yet you want to
see what impact on front-end modifying its query handler have? Don't worry, *Impactifier* got your back.
## Key Features
@@ -43,9 +28,15 @@ Your contracts generator does weird stuff under the hood, yet you want to see wh
- **Integration with CI/CD Pipelines:** Seamlessly integrate with GitHub Actions to provide impact reports in pull requests.
- **Performance:** Built in Rust for high performance and low latency. After all, it is all about faster releases.
## Getting Started
## Roadmap
We want to support specifying more detailed context of analysis, such as:
- specific file and directory/file:
\`$ impactifier . features/auth --to-branch develop\` - Analyse current file and its impact regarding
current state of \`--to-branch\`'s \`./features/auth\` directory
> Please note, that it doesn't yet work at all. Stuff below is meant to show how it *hopefully* will be used.
## Getting Started
### CI/CD
@@ -75,7 +66,7 @@ jobs:
\`\`\`
Flags information can easily be extracted inside the github action itself.
Full example can be found [here](github.com/wzslr321/impactifier/.github/impactifier.yaml)
Full example can be found [here](github.com/wzslr321/impactifier/example/.github/impactifier-action.yaml)
### CLI
@@ -112,45 +103,49 @@ Below is an example of a impactifier-config.yaml file:
\`\`\`yaml
repository:
url: "https://github.com/wzslr321/impactifier"
url: "https://github.com/example/repository.git"
path: "/path/to/local/repository"
access_token: "${GITHUB_ACCESS_TOKEN}"
options:
clone_into: "./repo"
rules:
- name: "Detect API Changes"
trigger:
path: "api/"
pattern: "func (\\w+)Handler"
transform:
name: "toApiEndpoint"
steps:
- name: "toLowerCase"
- name: "replace"
args:
pattern: "Handler$"
with: "_endpoint"
- name: "prepend"
args:
value: "/api/"
- name: "customFunction"
args:
script: |
fn transform(context) {
if context.class_name == "SpecialClass" {
return "/special" + context.matched_string;
} else {
return context.matched_string;
}
}
matcher:
path: "client/"
pattern: "ApiClient.call('$transform')"
action:
alert_level: "Severe"
message: "API changed"
on:
- push
- pull_request
clone_into: "/tmp/clone"
\`\`\`
#### Configuration Options
**repository:** Contains details about the repository to be analyzed.
- \`url\`: The URL of the repository to clone. This can be omitted if you provide a path.
- \`path\`: The path to a local repository. This can be used instead of cloning from a URL.
- \`access_token\`: An optional access token used for cloning private repositories. This can be set via an environment variable (e.g., \`${GITHUB_ACCESS_TOKEN}\`).
**options:** General options for Impactifier.
- \`on\`: A list of actions (push, pull_request) that trigger the analysis.
- \`clone_into\`: Specifies the directory where the repository should be cloned.
#### Overriding Configuration with CLI Flags
While the configuration file provides a convenient way to manage settings, you can override any of these options directly from the command line using CLI flags. This allows for flexibility, especially when running Impactifier in different environments (e.g., local vs. CI/CD).
For example:
\`\`\`sh
$ impactifier --config my-config.yaml --from-branch develop --to-branch main
\`\`\`
In the above command:
- \`--config my-config.yaml\`: Specifies a custom configuration file.
- \`--from-branch develop\` and \`--to-branch main\`: Override the branches defined in the configuration file.
**Priority Order**
Impactifier follows a specific priority order when determining which settings to use:
- CLI Arguments/Flags: Highest priority. Values provided here override both default settings and those in the configuration file.
- Configuration File: If no CLI argument is provided for a setting, Impactifier looks for it in the configuration file.
- Default Configuration File: If you don’t specify a configuration file with the --config flag, Impactifier looks for a file
named impactifier-config.yaml in the current working directory. If no file is found, it uses the default settings.
- Defaults: If neither a CLI argument nor a configuration file value is provided, Impactifier falls back to its default settings.
## Contributing
We welcome contributions to Impactifier! Please refer to our [Contributing Guidelines](CONTRIBUTING.md) for instructions on how to contribute.
diff --git a/impactifier-config.yaml b/impactifier-config.yaml
index 9a30634..ee0d25e 100644
--- a/impactifier-config.yaml
+++ b/impactifier-config.yaml
@@ -1,5 +1,6 @@
repository:
url: "https://github.com/wzslr321/impactifier"
branch: "main"
options:
clone_into: "./repo"
@@ -35,3 +36,4 @@ rules:
action:
alert_level: "Severe"
message: "API changed"
diff --git a/src/cli.rs b/src/cli.rs
index 1ec7843..f9aa3af 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,16 +1,20 @@
use std::fs::File;
use std::io::Write;
use std::path::Path;
use clap::Parser;
use git2::Repository;
use thiserror::Error;
use tracing::{error, info, trace, Level};
use url::Url;
use uuid::Uuid;
use serde_json::to_string_pretty;
use crate::config::{Config, RepositoryConfig};
use crate::git::clone_repo;
use crate::transform::init_registry;
use crate::utils;
use crate::{config::Config, git::clone_repo, git::extract_difference, git::open_repo};
#[derive(Parser, Debug)]
#[derive(Parser, Debug, Clone)]
#[command(
version,
about = "Impactifier is a tool for analyzing code changes and assessing their impact.",
@@ -26,7 +30,7 @@ use crate::utils;
creates repository struct from local path
from_branch fallbacks to default after opening
if branches specified & local changes detected, optionally includes those
if no branch & commit specified, tries to analyze local changes
if no branch & commit specified, tries (not yet) to analyze local changes
if no local changes fails as there is nothing to compare
"#
)]
@@ -38,16 +42,23 @@ struct Args {
#[arg(short, long, default_value_t = String::from("impactifier-config.yaml"))]
config: String,
/// From what branch changes should be compared.
///
/// Defaults to the current branch.
#[arg(short, long)]
from_branch: Option<String>,
/// To what branch changes should be compared
#[arg(long)]
to_branch: Option<String>,
/// Commit of which changes should be analyzed. Takes precedence over
/// branch changes, if \`from_branch\` or \`to_branch\` is specified.
#[arg(long)]
of_commit: Option<String>,
#[arg(long, help = "Fetch latest changes before comparison")]
/// Fetch last changes before impact analysis
#[arg(long)]
fetch: bool,
/// Sets max tracing level. Available options:
@@ -59,12 +70,33 @@ struct Args {
/// 4 = Error
#[arg(long, default_value_t = 2)]
tracing_level: u8,
#[arg(long, default_value_t=String::from("origin"))]
origin: String,
}
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
// TODO add more credentials variants
pub enum Credentials<'a> {
UsernamePassword {
username: &'a str,
password: &'a str,
},
}
pub fn run() -> Result<(), CliError> {
let args = Args::parse();
setup_logging(args.tracing_level);
match check_args_validity(args.clone()) {
Ok(_) => {
trace!("args validation completed successfully. Continuing execution.");
}
Err(err) => {
error!("args are invalid. Exiting...");
return Err(err);
}
}
let cfg = match load_config(Path::new(&args.config)) {
Ok(config) => config,
Err(e) => return Err(e),
@@ -72,120 +104,146 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
init_registry(cfg.custom_transform_scripts());
let _repository = match &cfg.repository.url {
Some(url) => {
match try_retrieve_repo_from_url(
url.as_str(),
args.from_branch,
args.to_branch,
args.of_commit,
cfg.options.clone_into,
&cfg.repository,
) {
Ok(repo) => repo,
Err(_) => {
return Err(Box::from("Either repository url or path must be specified"));
}
let repository = match cfg.repository.url {
Some(url) => match try_retrieve_repo_from_url(
cfg.repository.access_token,
"wzslr321",
&url,
cfg.options.clone_into,
) {
Ok(repo) => repo,
Err(e) => {
return Err(e);
}
}
None => match &cfg.repository.path {
Some(path) => {
match try_retrieve_repo_from_path(
(*path)
.to_str()
.expect("Path is expected to be validated during serialization"),
) {
Ok(repo) => repo,
Err(_) => {
return Err(Box::from("Either repository url or path must be specified"));
}
},
None => match cfg.repository.path {
Some(path) => match try_retrieve_repo_from_path(path) {
Ok(repo) => repo,
Err(err) => {
return Err(CliError::IncorrectArgs {
msg: "Either repository url or path must be specified".to_string(),
err: Some(err.into()),
});
}
}
},
None => {
error!("Repository url and path are unspecified");
return Err(Box::from("Either repository url or path must be specified"));
return Err(CliError::InvalidConfigPath { err: None });
}
},
};
Ok(())
}
let mock_credentials = Credentials::UsernamePassword {
username: "wzslr321",
password: "TEST",
};
fn try_retrieve_repo_from_path(path: &str) -> Result<Repository, Box<dyn std::error::Error>> {
trace!(
"attempt to retireve repo path specified repository.\nPath:{}",
path
);
todo!();
crate::git::fetch_remote(&repository, &args.origin, &mock_credentials).unwrap();
let diff = extract_difference(
&repository,
&crate::git::DiffOptions::Branches {
from: &args.from_branch.unwrap(),
to: &args.to_branch.unwrap_or_else(|| "main".to_string()),
},
)
.unwrap();
let serialized_diff = to_string_pretty(&diff).unwrap();
let mut file = File::create("./diff.json").unwrap();
file.write_all(serialized_diff.as_bytes()).unwrap();
Ok(())
}
fn try_retrieve_repo_from_url(
url: &str,
from_branch: Option<String>,
to_branch: Option<String>,
commit_id: Option<String>,
clone_into: Option<Box<Path>>,
config: &RepositoryConfig,
) -> Result<Repository, Box<dyn std::error::Error>> {
trace!("attempt to start from url-specified repository");
match (from_branch, to_branch) {
fn check_args_validity(args: Args) -> Result<(), CliError> {
match (&args.from_branch, &args.to_branch) {
(None, None) => {
info!("No branches specified");
match commit_id {
Some(_commit_id) => todo!(), //try_analyze_commit(&commit_id),
trace!("No branches specified");
match &args.of_commit {
Some(_) => Ok(()),
None => {
error!("No commit specified. Nothing to analyze");
Err(Box::from(
"No branches and no commit specified. Nothing to analyze.",
))
error!("Neither commit nor branch specified. Nothing to analyze");
Err(CliError::InsufficientArgs)
}
}
}
(None, Some(_)) => todo!(),
(None, Some(_)) => Ok(()),
(Some(_), None) => {
error!("Incorrect CLI arguments. Specifying \`from_branch\` requires \`to_branch\` to be specified");
Err(Box::from("Incorrect arguments"))
error!("from_branch specified, but to_branch is missing");
Err(CliError::IncorrectArgs {
msg: "Specifying \`from_branch\` requires \`to_branch\` to be specified".to_string(),
err: None,
})
}
(Some(_), Some(_)) => Ok(()),
}
}
fn try_retrieve_repo_from_path(path: Box<Path>) -> Result<Repository, CliError> {
match open_repo(&path) {
Ok(repository) => {
info!("sucessfully retrieved repository from path");
Ok(repository)
}
(Some(from_branch), Some(to_branch)) => {
info!(
"Attempting to compare branch {} with branch {}",
from_branch, to_branch
Err(err) => {
error!(
"failed to retrieve repository from path: {}",
String::from((*path).to_string_lossy())
);
info!("Attempting to clone repository from url: {}", url);
let clone_into_path = &clone_into.unwrap_or_else(|| {
let path = Path::new(&format!("repository{}", Uuid::new_v4())).into();
trace!("set fallback clone_into path to {:?}", path);
path
});
match utils::prepare_directory(&clone_into_path) {
Ok(_) => {
trace!("Starting to clone repository");
let cloned_repo =
match clone_repo(&config, &clone_into_path, Some(&from_branch)) {
Ok(repo) => repo,
Err(e) => {
error!("Failed to clone repository. error: {}", e);
return Err(e.into());
}
};
info!("Repository cloned successfuly");
Ok(cloned_repo)
}
Err(e) => {
error!("Failed to prepare directory for cloning");
return Err(e.into());
}
}
Err(CliError::IncorrectArgs {
msg: "Failed to retireve repository from path".to_string(),
err: Some(err.into()),
})
}
}
}
fn try_analyze_commit(_commit_id: &str) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
fn try_retrieve_repo_from_url(
access_token: Option<String>,
username: &str,
url: &Url,
clone_into: Option<Box<Path>>,
) -> Result<Repository, CliError> {
trace!("attempt to start from url-specified repository");
let clone_into_path = &clone_into.unwrap_or_else(|| {
let path = Path::new(&format!("repository{}", Uuid::new_v4())).into();
trace!("set fallback clone_into path to {:?}", path);
path
});
match utils::prepare_directory(&clone_into_path) {
Ok(_) => {
trace!("Starting to clone repository");
let credentials = Credentials::UsernamePassword {
username,
password: &access_token.unwrap_or_else(|| "OnlyForTesting".to_string()), // ehttps://www.twitch.tv/directory/followingxpect("access_token must be specified, as it is the only supported authentication method for now"),
};
let cloned_repo = match clone_repo(&credentials, url, &clone_into_path) {
Ok(repo) => repo,
Err(e) => {
error!("Failed to retreive repository from url.\nError: {}", e);
let err = match e {
crate::git::GitError::NoAccess { err } => CliError::InvalidArgs {
err: Some(err.into()),
},
fn try_analyze_local_changes() -> Result<(), Box<dyn std::error::Error>> {
todo!()
_ => CliError::Unknown {
err: Some(e.into()),
},
};
return Err(err);
}
};
info!("Repository retrieved successfuly from url");
Ok(cloned_repo)
}
Err(err) => {
error!("Failed to prepare directory for cloning");
return Err(CliError::Unknown { err: Some(err) });
}
}
}
fn setup_logging(tracing_level: u8) {
@@ -203,16 +261,33 @@ fn setup_logging(tracing_level: u8) {
.init();
}
fn load_config(path: &Path) -> Result<Config, Box<dyn std::error::Error>> {
fn load_config(path: &Path) -> anyhow::Result<Config, CliError> {
trace!("Starting loading config from {:?}", path);
match Config::load_from_file(path) {
Ok(config) => {
info!("Config loaded successfully");
Ok(config)
}
Err(e) => {
Err(err) => {
error!("Failed to read configuration from {:?}", path);
return Err(e.into());
return Err(CliError::InvalidConfigPath { err: Some(err) });
}
}
}
#[derive(Error, Debug)]
pub enum CliError {
#[error("No branches and no commit specified. No local changes detected. Nothing to analyze.")]
InsufficientArgs,
#[error("Incorrect CLI arguments.{}\nError:{:?}", msg, err)]
IncorrectArgs {
msg: String,
err: Option<anyhow::Error>,
},
#[error("Invalid arguments. Error:{:?}", err)]
InvalidArgs { err: Option<anyhow::Error> },
#[error("Config can not be retrieved")]
InvalidConfigPath { err: Option<anyhow::Error> },
#[error("Unknown error: {:?}", err)]
Unknown { err: Option<anyhow::Error> },
}
diff --git a/src/config.rs b/src/config.rs
index 75c9091..c94c62f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,12 +1,20 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Deserializer};
use tracing::error;
use std::cmp;
use std::env;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use tracing::debug;
use url::Url;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to read config from path: {}. Error:{}", path, msg)]
ReadFailure { path: String, msg: String },
}
#[derive(Debug, Deserialize)]
pub struct Config {
pub repository: RepositoryConfig,
@@ -20,7 +28,6 @@ pub struct RepositoryConfig {
pub url: Option<Url>,
pub path: Option<Box<Path>>,
pub access_token: Option<String>,
pub branch: String,
}
#[derive(Debug, Deserialize)]
@@ -88,18 +95,22 @@ pub struct CustomStep {
}
impl Config {
pub fn load_from_file(file_path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
// TODO(wiktor.zajac) improve error handling
pub fn load_from_file(file_path: &Path) -> Result<Self> {
let yaml_content = match std::fs::read_to_string(file_path) {
Ok(content) => {
debug!("Succesfully read yaml config file:\n{}", content);
content
}
Err(e) => {
return Err(e.into());
error!("Failed to read yaml config");
return Err(ConfigError::ReadFailure {
path: String::from(file_path.to_string_lossy()),
msg: e.to_string(),
}
.into())
}
};
let yaml_content = replace_env_vars(&yaml_content);
debug!("replaced env variables in yaml config");
let cfg = serde_yaml::from_str(&yaml_content)?;
debug!("Deserialized config:\n{}", cfg);
@@ -124,7 +135,7 @@ impl Config {
.and_then(|args| args.get("script"))
.and_then(|script_value| script_value.as_str())
.map(|s| s.to_string())
.unwrap()
.unwrap(),
})
})
.collect();
@@ -172,18 +183,6 @@ impl fmt::Display for Config {
}
}
// TODO: Probably could be globally handled with regex,
// but it first needs to be ensured that the performance
// will not be affected. If somehow the regex will affect
// performance strong enough, replacable variables should
// be predefined instead of being hardcoded here.
fn replace_env_vars(yaml_content: &str) -> String {
yaml_content.replace(
"${GITHUB_ACCESS_TOKEN}",
&env::var("GITHUB_ACCESS_TOKEN").unwrap_or_default(),
)
}
fn deserialize_url<'a, D>(deserializer: D) -> Result<Option<Url>, D::Error>
where
D: Deserializer<'a>,
diff --git a/src/git.rs b/src/git.rs
index 0e4e1fc..499e20f 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -1,50 +1,187 @@
use std::{env, path::Path};
use std::path::Path;
use serde::Serialize;
use thiserror::Error;
use git2::{Cred, RemoteCallbacks, Repository};
use tracing::error;
use std::str;
use tracing::{error, info, trace};
use url::Url;
use crate::config::RepositoryConfig;
use crate::cli::Credentials;
pub fn clone_repo(
options: &RepositoryConfig,
clone_into: &Path,
branch: Option<&str>,
) -> Result<Repository, Box<dyn std::error::Error>> {
let token = match options.access_token.to_owned() {
Some(token) => token,
None => match env::var("GITHUB_ACCESS_TOKEN_2") {
Ok(token) => token,
Err(_) => {
return Err(Box::from("No access token provided"));
#[derive(Error, Debug)]
pub enum GitError {
#[error("Failed to authorize git request, due to authentication failure. Error:{err}")]
NoAccess { err: git2::Error },
#[error(
"Failed to clone repository from url {} to given path: {}.\nError: {}",
url,
path,
err
)]
CloneFailure {
url: String,
path: String,
err: git2::Error,
},
#[error("Failed to open repository from path: {}. Error: {}", path, err)]
OpenRepositoryFailure { path: String, err: git2::Error },
// #[error("Unknown error: {}", *err)]
// Unknown { err: Box<dyn std::error::Error> },
}
#[derive(Debug, Serialize)]
pub struct Diff {
pub deltas: Vec<FileDelta>,
}
#[derive(Debug, Serialize)]
pub struct FileDelta {
pub value: String,
}
impl FileDelta {
fn from(value: String) -> Self {
Self { value }
}
}
pub enum DiffOptions<'a> {
Branches { from: &'a str, to: &'a str },
}
pub fn extract_difference(repo: &Repository, options: &DiffOptions) -> anyhow::Result<Diff> {
match options {
DiffOptions::Branches { from, to } => extract_difference_branches(repo, from, to),
}
}
pub fn fetch_remote(repo: &Repository, remote_name: &str, credentials: &Credentials) -> anyhow::Result<()> {
// Find the remote
let mut remote = repo.find_remote(remote_name)?;
// Set up callbacks for authentication (if needed)
let mut cb = RemoteCallbacks::new();
cb.credentials(|_url, _username_from_url, _allowed_types| credentials.into());
// Configure fetch options with the callbacks
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(cb);
// Define the refspecs to fetch. Here, we fetch all branches.
let refspecs = ["+refs/heads/*:refs/remotes/origin/*"];
// Perform the fetch
remote.fetch(&refspecs, Some(&mut fetch_options), None)?;
Ok(())
}
pub fn extract_difference_branches(
repo: &Repository,
from_branch: &str,
to_branch: &str,
) -> anyhow::Result<Diff> {
let ref_from = repo.find_reference(&format!("refs/heads/{}", from_branch))?;
let ref_to = repo.find_reference(&format!("refs/remotes/origin/{}", to_branch))?;
let commit_a = ref_from.peel_to_commit()?;
let commit_b = ref_to.peel_to_commit()?;
let tree_a = commit_a.tree()?;
let tree_b = commit_b.tree()?;
let diff = repo.diff_tree_to_tree(Some(&tree_a), Some(&tree_b), None)?;
let mut diff_output = Vec::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
diff_output.extend_from_slice(line.content());
true
})?;
let diff_str = str::from_utf8(&diff_output)
.map_err(|e| git2::Error::from_str(&format!("UTF-8 conversion error: {}", e)))?
.to_string();
Ok(Diff {
deltas: vec![FileDelta::from(diff_str)],
})
}
pub fn open_repo(path: &Path) -> Result<Repository, GitError> {
info!("start opening repository");
match Repository::open(path) {
Ok(repository) => {
trace!("repository opened successfuly");
Ok(repository)
}
Err(err) => {
error!("failed to open repository");
Err(GitError::OpenRepositoryFailure {
path: String::from(path.to_string_lossy()),
err,
})
}
}
}
impl Credentials<'_> {
fn into(&self) -> Result<Cred, git2::Error> {
let credentials = match self {
Credentials::UsernamePassword { username, password } => {
Cred::userpass_plaintext(&username, &password)
}
},
};
};
match credentials {
Ok(credentials) => Ok(credentials),
Err(err) => Err(err),
}
}
}
pub fn clone_repo(
credentials: &Credentials,
url: &Url,
clone_into: &Box<Path>,
) -> Result<Repository, GitError> {
info!("start cloning repository");
let mut callbacks = RemoteCallbacks::new();
callbacks.credentials(|_url, _username_from_url, _allowed_types| {
Cred::userpass_plaintext("wzslr321", &token)
});
// TODO(wiktor.zajac) [https://github.com/wzslr321/impactifier/issues/9]
// Support different credentials for git access
//
// Additionally, it can probably be extracted to a separate util
callbacks.credentials(|_url, _username_from_url, _allowed_types| credentials.into());
trace!("Callback credentials set to userpass_plaintext");
let mut builder = git2::build::RepoBuilder::new();
builder.bare(true);
if let Some(branch) = branch {
builder.branch(&branch);
}
let mut fetch_options = git2::FetchOptions::new();
fetch_options.remote_callbacks(callbacks);
fetch_options.depth(1);
builder.fetch_options(fetch_options);
// TODO(wiktor.zajac) try to guard agains future changes, to update this trace automatically
// by using some implemented trait with display
trace!("FetchOptions set to depth=0");
match &options.url {
Some(url) => match builder.clone(url.as_str(), &clone_into) {
Ok(repository) => Ok(repository),
Err(e) => Err(e.into()),
},
None => {
error!("Failed to clone the repository. Url not specified.");
Err(Box::from("Repository url not specified"))
match builder.clone(url.as_str(), &clone_into) {
Ok(repository) => {
info!("repository cloned successfully");
Ok(repository)
}
Err(e) => {
error!("failed to clone repository");
let err = match e.code() {
git2::ErrorCode::Auth => GitError::NoAccess { err: e },
_ => GitError::CloneFailure {
url: url.to_string(),
path: String::from(clone_into.to_string_lossy()),
err: e,
},
};
Err(err)
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 6e7638f..d8d2e36 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,8 +4,8 @@ mod git;
mod utils;
mod transform;
use std::error::Error;
use cli::CliError;
fn main() -> Result<(), Box<dyn Error>> {
cli::run()
fn main() -> Result<(), CliError> {
cli::run()
}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 2263e79..7049829 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -1,7 +1,7 @@
use std::{fs, path::Path};
use tracing::{info, trace};
pub fn prepare_directory(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
pub fn prepare_directory(path: &Path) -> Result<(), anyhow::Error> {
if path.exists() {
if path.read_dir()?.next().is_some() {
info!("Directory is not empty, removing existing files...");
|
lol worked |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
No description provided.