├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE └── src ├── format.rs ├── format └── callrustfmt.rs ├── git.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "autocfg" 5 | version = "1.0.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 8 | 9 | [[package]] 10 | name = "bitflags" 11 | version = "1.2.1" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 14 | 15 | [[package]] 16 | name = "cc" 17 | version = "1.0.50" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 20 | dependencies = [ 21 | "jobserver", 22 | ] 23 | 24 | [[package]] 25 | name = "cfg-if" 26 | version = "0.1.10" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 29 | 30 | [[package]] 31 | name = "git2" 32 | version = "0.11.0" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "77519ef7c5beee314d0804d4534f01e0f9e8d9acdee2b7a48627e590b27e0ec4" 35 | dependencies = [ 36 | "bitflags", 37 | "libc", 38 | "libgit2-sys", 39 | "log", 40 | "openssl-probe", 41 | "openssl-sys", 42 | "url", 43 | ] 44 | 45 | [[package]] 46 | name = "gitfmt" 47 | version = "0.1.0" 48 | dependencies = [ 49 | "git2", 50 | ] 51 | 52 | [[package]] 53 | name = "idna" 54 | version = "0.2.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 57 | dependencies = [ 58 | "matches", 59 | "unicode-bidi", 60 | "unicode-normalization", 61 | ] 62 | 63 | [[package]] 64 | name = "jobserver" 65 | version = "0.1.21" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" 68 | dependencies = [ 69 | "libc", 70 | ] 71 | 72 | [[package]] 73 | name = "libc" 74 | version = "0.2.66" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 77 | 78 | [[package]] 79 | name = "libgit2-sys" 80 | version = "0.10.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "d9ec6bca50549d34a392611dde775123086acbd994e3fff64954777ce2dc2e51" 83 | dependencies = [ 84 | "cc", 85 | "libc", 86 | "libssh2-sys", 87 | "libz-sys", 88 | "openssl-sys", 89 | "pkg-config", 90 | ] 91 | 92 | [[package]] 93 | name = "libssh2-sys" 94 | version = "0.2.14" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "36aa6e813339d3a063292b77091dfbbb6152ff9006a459895fa5bebed7d34f10" 97 | dependencies = [ 98 | "cc", 99 | "libc", 100 | "libz-sys", 101 | "openssl-sys", 102 | "pkg-config", 103 | "vcpkg", 104 | ] 105 | 106 | [[package]] 107 | name = "libz-sys" 108 | version = "1.0.25" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" 111 | dependencies = [ 112 | "cc", 113 | "libc", 114 | "pkg-config", 115 | "vcpkg", 116 | ] 117 | 118 | [[package]] 119 | name = "log" 120 | version = "0.4.8" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 123 | dependencies = [ 124 | "cfg-if", 125 | ] 126 | 127 | [[package]] 128 | name = "matches" 129 | version = "0.1.8" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 132 | 133 | [[package]] 134 | name = "openssl-probe" 135 | version = "0.1.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" 138 | 139 | [[package]] 140 | name = "openssl-sys" 141 | version = "0.9.54" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "1024c0a59774200a555087a6da3f253a9095a5f344e353b212ac4c8b8e450986" 144 | dependencies = [ 145 | "autocfg", 146 | "cc", 147 | "libc", 148 | "pkg-config", 149 | "vcpkg", 150 | ] 151 | 152 | [[package]] 153 | name = "percent-encoding" 154 | version = "2.1.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 157 | 158 | [[package]] 159 | name = "pkg-config" 160 | version = "0.3.17" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 163 | 164 | [[package]] 165 | name = "smallvec" 166 | version = "1.2.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" 169 | 170 | [[package]] 171 | name = "unicode-bidi" 172 | version = "0.3.4" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 175 | dependencies = [ 176 | "matches", 177 | ] 178 | 179 | [[package]] 180 | name = "unicode-normalization" 181 | version = "0.1.12" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 184 | dependencies = [ 185 | "smallvec", 186 | ] 187 | 188 | [[package]] 189 | name = "url" 190 | version = "2.1.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 193 | dependencies = [ 194 | "idna", 195 | "matches", 196 | "percent-encoding", 197 | ] 198 | 199 | [[package]] 200 | name = "vcpkg" 201 | version = "0.2.8" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" 204 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitfmt" 3 | version = "0.1.0" 4 | authors = ["Alexander Theißen "] 5 | edition = "2018" 6 | 7 | [profile.release] 8 | lto = true 9 | 10 | [profile.bench] 11 | lto = true 12 | 13 | [dependencies] 14 | git2 = "0.11" 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alexander Theißen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{self, Display}; 3 | use std::path::PathBuf; 4 | 5 | mod callrustfmt; 6 | 7 | pub struct Hunk { 8 | pub start: u32, 9 | pub lines: u32, 10 | } 11 | 12 | pub struct Formatters<'a> { 13 | repo: &'a [Formatter], 14 | fmts: HashMap<&'static str, FmtHunks<'a>>, 15 | } 16 | 17 | type Hunks = HashMap>; 18 | 19 | struct FmtHunks<'a> { 20 | fmt: &'a Formatter, 21 | hunks: Hunks, 22 | } 23 | 24 | impl<'a> Formatters<'a> { 25 | pub fn ext_supported(&self, ext: &str) -> bool { 26 | self.fmts.contains_key(ext) 27 | } 28 | 29 | pub fn add_hunk(&mut self, ext: &str, path: PathBuf, hunk: Hunk) { 30 | if let Some(hunks) = self.fmts.get_mut(ext) { 31 | hunks.hunks.entry(path).or_insert(Vec::new()).push(hunk); 32 | } 33 | } 34 | } 35 | 36 | impl<'a> Formatters<'a> { 37 | pub fn new(repo: &'a [Formatter]) -> Self { 38 | let mut fmts = Formatters { 39 | repo, 40 | fmts: HashMap::new(), 41 | }; 42 | 43 | for fmt in fmts.repo.iter() { 44 | for ext in fmt.extensions() { 45 | let old = fmts.fmts.insert( 46 | *ext, 47 | FmtHunks { 48 | fmt, 49 | hunks: Hunks::new(), 50 | }, 51 | ); 52 | if let Some(old) = old { 53 | panic!( 54 | "Formatter {} tried to add extension already added by {}", 55 | *ext, old.fmt 56 | ); 57 | } 58 | } 59 | } 60 | fmts 61 | } 62 | 63 | pub fn format(self) { 64 | let mut merged: Vec = Vec::new(); 65 | for hunk in self.fmts { 66 | if let Some(hunks) = merged.iter_mut().find(|n| n.fmt == hunk.1.fmt) { 67 | hunks.merge(hunk.1); 68 | } else { 69 | merged.push(hunk.1); 70 | } 71 | } 72 | 73 | for fmt_hunk in merged.into_iter().filter(|x| !x.hunks.is_empty()) { 74 | fmt_hunk.format(); 75 | } 76 | } 77 | } 78 | 79 | impl<'a> FmtHunks<'a> { 80 | fn merge(&mut self, o: Self) { 81 | assert!(self.fmt == o.fmt); 82 | for mut hunks in o.hunks { 83 | if let Some(p) = self.hunks.get_mut(&hunks.0) { 84 | p.append(&mut hunks.1) 85 | } else { 86 | self.hunks.insert(hunks.0, hunks.1); 87 | } 88 | } 89 | } 90 | 91 | fn format(self) { 92 | self.fmt.format(self.hunks).unwrap(); 93 | } 94 | } 95 | 96 | pub trait Format: fmt::Display + std::cmp::PartialEq { 97 | fn extensions(&self) -> &'static [&'static str]; 98 | fn format(&self, p: Hunks) -> Result<(), String>; 99 | } 100 | 101 | #[derive(PartialEq)] 102 | pub enum Formatter { 103 | RustFmt(callrustfmt::CallRustFmt), 104 | } 105 | 106 | impl Display for Formatter { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | match self { 109 | Self::RustFmt(x) => x.fmt(f), 110 | } 111 | } 112 | } 113 | 114 | impl Format for Formatter { 115 | fn extensions(&self) -> &'static [&'static str] { 116 | match self { 117 | Self::RustFmt(x) => x.extensions(), 118 | } 119 | } 120 | 121 | fn format(&self, p: Hunks) -> Result<(), String> { 122 | match self { 123 | Self::RustFmt(x) => x.format(p), 124 | } 125 | } 126 | } 127 | 128 | pub fn construct_repo() -> Vec { 129 | vec![Formatter::RustFmt(callrustfmt::CallRustFmt {})] 130 | } 131 | -------------------------------------------------------------------------------- /src/format/callrustfmt.rs: -------------------------------------------------------------------------------- 1 | use super::{Format, Hunks}; 2 | use std::fmt; 3 | use std::process::Command; 4 | 5 | const EXTENSIONS: [&str; 1] = ["rs"]; 6 | const NAME: &str = "CallRustFmt"; 7 | 8 | pub struct CallRustFmt; 9 | 10 | impl Format for CallRustFmt { 11 | fn extensions(&self) -> &'static [&'static str] { 12 | &EXTENSIONS 13 | } 14 | 15 | fn format(&self, hunks: Hunks) -> Result<(), String> { 16 | let mut changes_json = String::new(); 17 | changes_json.push('['); 18 | for (path, hunks) in hunks.iter() { 19 | for hunk in hunks { 20 | changes_json.push_str(&format!( 21 | "{{\"file\":\"{}\",\"range\":[{},{}]}},", 22 | path.to_str().unwrap(), 23 | hunk.start, 24 | hunk.start + hunk.lines + 1, 25 | )); 26 | } 27 | } 28 | changes_json.pop(); 29 | changes_json.push(']'); 30 | 31 | Command::new("rustfmt") 32 | .args(&[ 33 | "+nightly", 34 | "--unstable-features", 35 | "--file-lines", 36 | &changes_json, 37 | "--skip-children", 38 | ]) 39 | .args(hunks.keys()) 40 | .status() 41 | .unwrap(); 42 | Ok(()) 43 | } 44 | } 45 | 46 | impl fmt::Display for CallRustFmt { 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 48 | write!(f, "{}", NAME) 49 | } 50 | } 51 | 52 | impl std::cmp::PartialEq for CallRustFmt { 53 | fn eq(&self, _: &Self) -> bool { 54 | true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/git.rs: -------------------------------------------------------------------------------- 1 | use crate::format::{Formatters, Hunk}; 2 | use git2::Delta; 3 | use git2::DiffOptions; 4 | use git2::Repository; 5 | use std::env::set_current_dir; 6 | 7 | pub fn collect_hunks(formatters: &mut Formatters) { 8 | let repo = Repository::open_from_env().unwrap(); 9 | set_current_dir(repo.workdir().unwrap()).unwrap(); 10 | 11 | let head = repo.head().unwrap().peel_to_tree().unwrap(); 12 | 13 | let mut diff_options = DiffOptions::new(); 14 | diff_options.include_untracked(true); 15 | diff_options.recurse_untracked_dirs(true); 16 | diff_options.show_untracked_content(true); 17 | diff_options.context_lines(2); 18 | let diff = repo 19 | .diff_tree_to_workdir(Some(&head), Some(&mut diff_options)) 20 | .unwrap(); 21 | 22 | diff.foreach( 23 | &mut |_, _| true, 24 | Some(&mut |_, _| true), 25 | Some(&mut |delta, hunk| { 26 | let ext = if let Some(ext) = delta.new_file().path().unwrap().extension() { 27 | ext.to_str().unwrap() 28 | } else { 29 | return true; 30 | }; 31 | 32 | if !formatters.ext_supported(ext) { 33 | return true; 34 | } 35 | 36 | match delta.status() { 37 | Delta::Added 38 | | Delta::Modified 39 | | Delta::Renamed 40 | | Delta::Copied 41 | | Delta::Untracked 42 | | Delta::Ignored => { 43 | let h = Hunk { 44 | start: hunk.new_start(), 45 | lines: hunk.new_lines(), 46 | }; 47 | let path = delta.new_file().path().unwrap().to_path_buf(); 48 | formatters.add_hunk(ext, path, h); 49 | true 50 | } 51 | _ => true, 52 | } 53 | }), 54 | None, 55 | ) 56 | .unwrap(); 57 | } 58 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Based on: https://gist.github.com/zroug/28605f45a662b483fb4a7c3545627f66 2 | 3 | mod format; 4 | mod git; 5 | 6 | fn main() { 7 | let formatters = format::construct_repo(); 8 | let mut formatters = format::Formatters::new(&formatters); 9 | 10 | git::collect_hunks(&mut formatters); 11 | formatters.format(); 12 | } 13 | --------------------------------------------------------------------------------