Skip to content

Commit

Permalink
Merge pull request #295 from siketyan/feat/exclude-rule
Browse files Browse the repository at this point in the history
feat: Add entries to .git/info/exclude from the profile
  • Loading branch information
siketyan authored Jan 2, 2024
2 parents 820bb1c + c97a066 commit 8cf800f
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 7 deletions.
6 changes: 6 additions & 0 deletions ghr.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ infer = false
user.name = "My Working Name"
user.email = "[email protected]"

# Adds entries to .git/info/exclude (not .gitignore).
excludes = [
"/.idea/",
".DS_Store",
]

[applications.vscode]
# You can open a repository in VS Code using `ghr open <repo> vscode`.
cmd = "code"
Expand Down
3 changes: 1 addition & 2 deletions src/cmd/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,8 @@ impl Cmd {
);

let repo = Repository::open(&path)?;

if let Some((name, p)) = profile {
p.apply(&mut repo.config()?)?;
p.apply(&repo)?;

info!("Attached profile [{}] successfully.", style(name).bold());
}
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ impl Cmd {

let repo = Repository::open(&path)?;
let profile = if let Some((name, p)) = profile {
p.apply(&mut repo.config()?)?;
p.apply(&repo)?;
Some(name.to_string())
} else {
None
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl Cmd {
);

if let Some((name, p)) = profile {
p.apply(&mut repo.config()?)?;
p.apply(&repo)?;

info!("Attached profile [{}] successfully.", style(name).bold());
}
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/profile/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ impl Cmd {
.ok_or_else(|| anyhow!("Unknown profile: {}", &self.name))?;

let repo = Repository::open_from_env()?;
profile.apply(&repo)?;

profile.apply(&mut repo.config()?)?;
info!(
"Attached profile [{}] successfully.",
style(self.name).bold()
Expand Down
201 changes: 201 additions & 0 deletions src/git/exclude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use std::convert::Infallible;
use std::fmt::{Display, Result as FmtResult};
use std::fs::File as StdFile;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::str::FromStr;

use anyhow::Result;

#[derive(Debug, Eq, PartialEq)]
pub enum Node {
Empty,
Comment(String),
Include(String),
Exclude(String),
}

impl FromStr for Node {
type Err = Infallible;

fn from_str(s: &str) -> StdResult<Self, Self::Err> {
let mut chars = s.chars();

Ok(match chars.next() {
Some('#') => Node::Comment(chars.collect()),
Some('!') => Node::Include(chars.collect()),
Some(_) => Node::Exclude(s.to_string()),
None => Node::Empty,
})
}
}

impl Display for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> FmtResult {
let str = match self {
Node::Empty => String::new(),
Node::Comment(c) => format!("#{}", c),
Node::Include(i) => format!("!{}", i),
Node::Exclude(e) => e.to_string(),
};

write!(f, "{}", str)
}
}

#[derive(Debug, Default)]
pub struct File {
nodes: Vec<Node>,
}

impl File {
pub fn read<R>(reader: R) -> Result<Self>
where
R: Read,
{
let reader = BufReader::new(reader);

Ok(Self {
nodes: reader
.lines()
.map(|l| l.map(|l| Node::from_str(&l).unwrap()))
.collect::<Result<Vec<_>, _>>()?,
})
}

pub fn load<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
Self::read(StdFile::open(Self::file_path(path))?)
}

pub fn write<W>(&self, writer: W) -> Result<()>
where
W: Write,
{
let mut writer = BufWriter::new(writer);
for node in &self.nodes {
writeln!(writer, "{}", node)?;
}

Ok(())
}

pub fn save<P>(&self, path: P) -> Result<()>
where
P: AsRef<Path>,
{
self.write(StdFile::create(Self::file_path(path))?)
}

pub fn add_or_noop(&mut self, node: Node) {
if !self.nodes.contains(&node) {
self.nodes.push(node);
}
}

fn file_path<P>(path: P) -> PathBuf
where
P: AsRef<Path>,
{
path.as_ref().join(".git").join("info").join("exclude")
}
}

impl Deref for File {
type Target = Vec<Node>;

fn deref(&self) -> &Self::Target {
&self.nodes
}
}

impl DerefMut for File {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.nodes
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;

#[test]
fn test_node_from_str() {
assert_eq!(
Node::Comment("This is a comment".to_string()),
Node::from_str("#This is a comment").unwrap(),
);
assert_eq!(
Node::Include("/path/to/**/file.ext".to_string()),
Node::from_str("!/path/to/**/file.ext").unwrap(),
);
assert_eq!(
Node::Exclude("/path/to/**/file.ext".to_string()),
Node::from_str("/path/to/**/file.ext").unwrap(),
);
}

#[test]
fn test_node_to_string() {
assert_eq!(
"#This is a comment".to_string(),
Node::Comment("This is a comment".to_string()).to_string(),
);
assert_eq!(
"!/path/to/**/file.ext".to_string(),
Node::Include("/path/to/**/file.ext".to_string()).to_string(),
);
assert_eq!(
"/path/to/**/file.ext".to_string(),
Node::Exclude("/path/to/**/file.ext".to_string()).to_string(),
);
}

#[test]
fn test_file_read() {
let content = br#"
# File patterns to ignore; see `git help ignore` for more information.
# Lines that start with '#' are comments.
.idea
"#;

assert_eq!(
vec![
Node::Empty,
Node::Comment(
" File patterns to ignore; see `git help ignore` for more information."
.to_string()
),
Node::Comment(" Lines that start with '#' are comments.".to_string()),
Node::Exclude(".idea".to_string()),
],
File::read(&content[..]).unwrap().nodes,
);
}

#[test]
fn test_file_write() {
let mut content = Vec::<u8>::new();
let mut file = File::default();

file.push(Node::Comment("This is a comment".to_string()));
file.push(Node::Empty);
file.push(Node::Include("/path/to/include".to_string()));
file.push(Node::Exclude("/path/to/exclude".to_string()));
file.write(&mut content).unwrap();

assert_eq!(
r#"#This is a comment
!/path/to/include
/path/to/exclude
"#,
String::from_utf8(content).unwrap(),
)
}
}
2 changes: 2 additions & 0 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod config;
mod strategy;

pub mod exclude;

pub use config::Config;

use std::path::Path;
Expand Down
16 changes: 14 additions & 2 deletions src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ use std::ops::Deref;
use std::result::Result as StdResult;

use anyhow::Result;
use git2::Config;
use git2::Repository;
use itertools::Itertools;
use serde::de::{MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use toml::value::Value;
use toml::Table;

use crate::git::exclude::{File, Node};
use crate::rule::ProfileRef;

#[derive(Debug, Default)]
Expand Down Expand Up @@ -125,12 +126,23 @@ impl<'de> Visitor<'de> for ConfigsVisitor {

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Profile {
#[serde(default)]
pub excludes: Vec<String>,
#[serde(default, flatten)]
pub configs: Configs,
}

impl Profile {
pub fn apply(&self, config: &mut Config) -> Result<()> {
pub fn apply(&self, repo: &Repository) -> Result<()> {
let path = repo.workdir().unwrap();
let mut exclude = File::load(path)?;
for value in &self.excludes {
exclude.add_or_noop(Node::Exclude(value.to_string()));
}

exclude.save(path)?;

let mut config = repo.config()?;
for (key, value) in &self.configs.map {
config.set_str(key, value)?;
}
Expand Down

0 comments on commit 8cf800f

Please sign in to comment.