├── .gitignore ├── test └── data │ ├── src │ └── main.rs │ ├── Cargo.lock │ ├── Cargo.toml │ └── .licensure.yml ├── Cross.toml ├── scripts ├── local_install.sh └── cross-compile.sh ├── Cargo.toml ├── src ├── utils │ └── mod.rs ├── comments │ ├── block_comment.rs │ ├── line_comment.rs │ └── mod.rs ├── config │ ├── default.rs │ ├── comment.rs │ ├── mod.rs │ └── license.rs ├── main.rs ├── licensure.rs └── template.rs ├── .github └── workflows │ ├── ci.yaml │ └── release.yml ├── .licensure.yml ├── README.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /test/data/target 3 | **/*.rs.bk 4 | .idea/ 5 | -------------------------------------------------------------------------------- /test/data/src/main.rs: -------------------------------------------------------------------------------- 1 | // used to test --check flag so should not have a license header 2 | fn main() { 3 | println!("Hello, world!"); 4 | } 5 | -------------------------------------------------------------------------------- /test/data/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "data" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /test/data/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "data" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | pre-build = [ 3 | "dpkg --add-architecture $CROSS_DEB_ARCH", 4 | "apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH", 5 | ] 6 | 7 | [target.arm-unknown-linux-gnueabihf] 8 | pre-build = [ 9 | "dpkg --add-architecture $CROSS_DEB_ARCH", 10 | "apt-get update && apt-get install --assume-yes libssl-dev:$CROSS_DEB_ARCH", 11 | ] 12 | -------------------------------------------------------------------------------- /scripts/local_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o pipefail 4 | set -x 5 | 6 | INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" 7 | 8 | REPO=$(git rev-parse --show-toplevel) 9 | 10 | cd "$REPO" || exit 1 11 | 12 | cargo build --release 13 | 14 | if [[ -x $(which strip) ]]; then 15 | strip ./target/release/licensure 16 | fi 17 | 18 | mv ./target/release/licensure "$INSTALL_DIR/" 19 | -------------------------------------------------------------------------------- /scripts/cross-compile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -x $(which cross 2>/dev/null) ]]; then 4 | echo "You need to install cross to use this script:" 5 | echo " https://github.com/cross-rs/cross" 6 | exit 1 7 | fi 8 | 9 | if [[ ! -x $(which docker 2>/dev/null) ]]; then 10 | echo "You need to install docker to use this script:" 11 | echo " https://docker.com" 12 | exit 1 13 | fi 14 | 15 | TARGETS=( 16 | "x86_64-unknown-linux-gnu" 17 | "arm-unknown-linux-gnueabihf" 18 | ) 19 | 20 | for target in "${TARGETS[@]}"; do 21 | cross build --release --target "$target" 22 | done -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Mathew Robinson "] 3 | name = "licensure" 4 | version = "0.8.1" 5 | keywords = ["licensing", "cli", "tool", "license"] 6 | license = "GPL-3.0" 7 | readme = "README.md" 8 | description = "A software license management tool" 9 | repository = "https://github.com/chasinglogic/licensure" 10 | homepage = "https://github.com/chasinglogic/licensure" 11 | edition = "2024" 12 | 13 | [dependencies] 14 | chrono = "0.4.40" 15 | regex = "1.11.1" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_yaml = "0.9.34" 18 | log = "0.4.26" 19 | simplelog = "0.12.2" 20 | ureq = { version = "3", features = ["json"] } 21 | textwrap = "0.16.2" 22 | serde_regex = "1.1.0" 23 | clap = { version = "4.5.32", features = ["derive"] } 24 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2024 Mathew Robinson 2 | // 3 | // This program is free software: you can redistribute it and/or modify it under 4 | // the terms of the GNU General Public License as published by the Free Software 5 | // Foundation, version 3. 6 | // 7 | // This program is distributed in the hope that it will be useful, but WITHOUT 8 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License along with 12 | // this program. If not, see . 13 | // 14 | use regex::Regex; 15 | 16 | pub fn remove_column_wrapping(string: &str) -> String { 17 | // Some license headers come pre-wrapped to a column width. 18 | // This regex replacement undoes the column-width wrapping 19 | // while preserving intentional line breaks / empty lines. 20 | let re = Regex::new(r"(?P.)\n").unwrap(); 21 | re.replace_all(string, "$char ").replace(" \n", "\n\n") 22 | } 23 | 24 | #[cfg(test)] 25 | mod tests { 26 | use crate::utils::remove_column_wrapping; 27 | 28 | #[test] 29 | fn test_remove_column_wrapping() { 30 | let content = "\ 31 | some wrapped 32 | text to unwrap. 33 | 34 | The line above 35 | is an intentional 36 | line break. 37 | 38 | So is this."; 39 | 40 | let expected = "some wrapped text to unwrap.\n\nThe line above \ 41 | is an intentional line break.\n\nSo is this."; 42 | assert_eq!(expected, remove_column_wrapping(content)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: test 8 | env: 9 | # Emit backtraces on panics. 10 | RUST_BACKTRACE: 1 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | build: 15 | # Test on stable and beta rust compilers. 16 | - stable 17 | - beta 18 | include: 19 | - build: macos 20 | os: macos-latest 21 | rust: stable 22 | - build: stable 23 | os: ubuntu-latest 24 | rust: stable 25 | - build: beta 26 | os: ubuntu-latest 27 | rust: beta 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | 32 | - name: Install Rust 33 | uses: dtolnay/rust-toolchain@master 34 | with: 35 | toolchain: ${{ matrix.rust }} 36 | 37 | - name: Build licensure 38 | run: cargo build --verbose ${{ env.TARGET_FLAGS }} 39 | 40 | - name: Run cargo tests 41 | run: cargo test 42 | 43 | clippy: 44 | name: clippy 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v3 49 | - name: Install Rust 50 | uses: dtolnay/rust-toolchain@master 51 | with: 52 | toolchain: stable 53 | components: clippy 54 | - name: Lint 55 | run: cargo clippy --no-deps --all-targets 56 | 57 | rustfmt: 58 | name: rustfmt 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout repository 62 | uses: actions/checkout@v3 63 | - name: Install Rust 64 | uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: stable 67 | components: rustfmt 68 | - name: Check formatting 69 | run: cargo fmt --all --check 70 | -------------------------------------------------------------------------------- /src/comments/block_comment.rs: -------------------------------------------------------------------------------- 1 | use crate::comments::line_comment::LineComment; 2 | 3 | use super::Comment; 4 | 5 | pub struct BlockComment { 6 | start: String, 7 | end: String, 8 | per_line: Option>, 9 | trailing_lines: usize, 10 | cols: Option, 11 | } 12 | 13 | impl BlockComment { 14 | pub fn new(start: &str, end: &str, cols: Option) -> BlockComment { 15 | BlockComment { 16 | start: String::from(start), 17 | end: String::from(end), 18 | per_line: None, 19 | trailing_lines: 0, 20 | cols, 21 | } 22 | } 23 | 24 | pub fn set_trailing_lines(mut self, num_lines: usize) -> BlockComment { 25 | self.trailing_lines = num_lines; 26 | self 27 | } 28 | 29 | pub fn with_per_line(mut self, per_line: &str) -> BlockComment { 30 | self.per_line = Some(Box::new( 31 | LineComment::new(per_line, self.cols).skip_trailing_lines(), 32 | )); 33 | self 34 | } 35 | } 36 | 37 | impl Comment for BlockComment { 38 | fn comment(&self, text: &str) -> String { 39 | let mut new_text = self.start.clone(); 40 | let wrapped_text; 41 | 42 | match self.per_line { 43 | Some(ref commenter) => { 44 | let commented_text = commenter.comment(text); 45 | new_text.push_str(&commented_text); 46 | } 47 | None => new_text.push_str(match self.cols { 48 | Some(cols) => { 49 | wrapped_text = textwrap::fill(text, cols); 50 | wrapped_text.as_str() 51 | } 52 | None => text, 53 | }), 54 | }; 55 | 56 | new_text.push_str(&self.end); 57 | 58 | for _ in 0..self.trailing_lines { 59 | new_text.push('\n'); 60 | } 61 | 62 | new_text 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.licensure.yml: -------------------------------------------------------------------------------- 1 | change_in_place: true 2 | # Regexes which if matched by a file path will always be excluded from 3 | # getting a license header 4 | excludes: 5 | - \.gitignore 6 | - .*lock 7 | - \.git/.* 8 | - \.licensure\.yml 9 | - README.* 10 | - LICENSE.* 11 | - .*\.(md|rst|txt) 12 | - Cargo.toml 13 | - .*\.github/.* 14 | # Definition of the licenses used on this project and to what files 15 | # they should apply. 16 | licenses: 17 | # Either a regex or the string "any" to determine to what files this 18 | # license should apply. It is common for projects to have files 19 | # under multiple licenses or with multiple copyright holders. This 20 | # provides the ability to automatically license files correctly 21 | # based on their file paths. 22 | # 23 | # If "any" is provided all files will match this license. 24 | - files: any 25 | ident: GPL-3.0 26 | authors: 27 | - name: Mathew Robinson 28 | email: chasinglogic@gmail.com 29 | auto_template: true 30 | 31 | # Define type of comment characters to apply based on file extensions. 32 | comments: 33 | # The extensions (or singular extension) field defines which file 34 | # extensions to apply the commenter to. 35 | - columns: 80 36 | extensions: 37 | - rs 38 | commenter: 39 | type: line 40 | comment_char: "//" 41 | trailing_lines: 0 42 | # In this case extension is singular and a single string extension is provided. 43 | - columns: 120 44 | extension: html 45 | commenter: 46 | type: block 47 | start_block_char: "" 49 | # The extension string "any" is special and so will match any file 50 | # extensions. Commenter configurations are always checked in the 51 | # order they are defined, so if any is used it should be the last 52 | # commenter configuration or else it will override all others. 53 | # 54 | # In this configuration if we can't match the file extension we fall 55 | # back to the popular "#" line comment used in most scripting 56 | # languages. 57 | - columns: 80 58 | extension: any 59 | commenter: 60 | type: line 61 | comment_char: "#" 62 | trailing_lines: 0 63 | 64 | -------------------------------------------------------------------------------- /test/data/.licensure.yml: -------------------------------------------------------------------------------- 1 | change_in_place: true 2 | # Regexes which if matched by a file path will always be excluded from 3 | # getting a license header 4 | excludes: 5 | - \.gitignore 6 | - .*lock 7 | - \.git/.* 8 | - \.licensure\.yml 9 | - README.* 10 | - LICENSE.* 11 | - .*\.(md|rst|txt) 12 | - Cargo.toml 13 | - .*\.github/.* 14 | # Definition of the licenses used on this project and to what files 15 | # they should apply. 16 | licenses: 17 | # Either a regex or the string "any" to determine to what files this 18 | # license should apply. It is common for projects to have files 19 | # under multiple licenses or with multiple copyright holders. This 20 | # provides the ability to automatically license files correctly 21 | # based on their file paths. 22 | # 23 | # If "any" is provided all files will match this license. 24 | - files: any 25 | ident: GPL-3.0 26 | authors: 27 | - name: Mathew Robinson 28 | email: chasinglogic@gmail.com 29 | auto_template: true 30 | 31 | # Define type of comment characters to apply based on file extensions. 32 | comments: 33 | # The extensions (or singular extension) field defines which file 34 | # extensions to apply the commenter to. 35 | - columns: 80 36 | extensions: 37 | - rs 38 | commenter: 39 | type: line 40 | comment_char: "//" 41 | trailing_lines: 0 42 | # In this case extension is singular and a single string extension is provided. 43 | - columns: 120 44 | extension: html 45 | commenter: 46 | type: block 47 | start_block_char: "" 49 | # The extension string "any" is special and so will match any file 50 | # extensions. Commenter configurations are always checked in the 51 | # order they are defined, so if any is used it should be the last 52 | # commenter configuration or else it will override all others. 53 | # 54 | # In this configuration if we can't match the file extension we fall 55 | # back to the popular "#" line comment used in most scripting 56 | # languages. 57 | - columns: 80 58 | extension: any 59 | commenter: 60 | type: line 61 | comment_char: "#" 62 | trailing_lines: 0 63 | 64 | -------------------------------------------------------------------------------- /src/comments/line_comment.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2024 Mathew Robinson 2 | // 3 | // This program is free software: you can redistribute it and/or modify it under 4 | // the terms of the GNU General Public License as published by the Free Software 5 | // Foundation, version 3. 6 | // 7 | // This program is distributed in the hope that it will be useful, but WITHOUT 8 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License along with 12 | // this program. If not, see . 13 | // 14 | use super::Comment; 15 | 16 | pub struct LineComment { 17 | character: String, 18 | trailing_lines: usize, 19 | cols: Option, 20 | } 21 | 22 | impl LineComment { 23 | pub fn new(character: &str, cols: Option) -> LineComment { 24 | LineComment { 25 | character: String::from(character), 26 | trailing_lines: 0, 27 | cols, 28 | } 29 | } 30 | 31 | pub fn set_trailing_lines(mut self, num_lines: usize) -> LineComment { 32 | self.trailing_lines = num_lines; 33 | self 34 | } 35 | 36 | pub fn skip_trailing_lines(mut self) -> LineComment { 37 | self.trailing_lines = 0; 38 | self 39 | } 40 | } 41 | 42 | impl Comment for LineComment { 43 | fn comment(&self, text: &str) -> String { 44 | let local_copy = match self.cols { 45 | Some(cols) => { 46 | // Subtract two columns to account for the comment 47 | // character and space we will add later. 48 | textwrap::fill(text, if cols > 2 { cols - 2 } else { cols }) 49 | } 50 | None => text.to_string(), 51 | }; 52 | 53 | let mut lines: Vec<&str> = local_copy.split('\n').collect(); 54 | // split always adds an empty element to the end of the vector 55 | // so we filter it out here. 56 | if !lines.is_empty() && lines.last().unwrap() == &"" { 57 | lines.pop(); 58 | } 59 | 60 | let mut new_text = "".to_string(); 61 | for line in lines { 62 | let new_line = match line { 63 | "" => format!("{}\n", self.character), 64 | _ => format!("{} {}\n", self.character, line), 65 | }; 66 | 67 | new_text.push_str(&new_line); 68 | } 69 | 70 | for _ in 0..self.trailing_lines { 71 | new_text.push('\n'); 72 | } 73 | 74 | new_text 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/comments/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2024 Mathew Robinson 2 | // 3 | // This program is free software: you can redistribute it and/or modify it under 4 | // the terms of the GNU General Public License as published by the Free Software 5 | // Foundation, version 3. 6 | // 7 | // This program is distributed in the hope that it will be useful, but WITHOUT 8 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License along with 12 | // this program. If not, see . 13 | // 14 | 15 | pub use block_comment::BlockComment; 16 | pub use line_comment::LineComment; 17 | 18 | mod block_comment; 19 | mod line_comment; 20 | 21 | pub trait Comment { 22 | fn comment(&self, text: &str) -> String; 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | 29 | const EX_TEXT: &str = "There once was a man 30 | with a very nice cat 31 | the cat wore a top hat 32 | it looked super dapper 33 | "; 34 | 35 | #[test] 36 | fn test_comment_python() { 37 | assert_eq!( 38 | "# There once was a man 39 | # with a very nice cat 40 | # the cat wore a top hat 41 | # it looked super dapper 42 | ", 43 | LineComment::new("#", None).comment(EX_TEXT) 44 | ) 45 | } 46 | 47 | #[test] 48 | fn test_comment_python_w_trailing_lines() { 49 | assert_eq!( 50 | "# There once was a man 51 | # with a very nice cat 52 | # the cat wore a top hat 53 | # it looked super dapper 54 | 55 | 56 | ", 57 | LineComment::new("#", None) 58 | .set_trailing_lines(2) 59 | .comment(EX_TEXT) 60 | ) 61 | } 62 | 63 | #[test] 64 | fn test_comment_cpp() { 65 | assert_eq!( 66 | "/* 67 | * There once was a man 68 | * with a very nice cat 69 | * the cat wore a top hat 70 | * it looked super dapper 71 | */", 72 | BlockComment::new("/*\n", "*/", None) 73 | .with_per_line("*") 74 | .comment(EX_TEXT) 75 | ) 76 | } 77 | 78 | #[test] 79 | fn test_comment_cpp_w_trailing_lines() { 80 | assert_eq!( 81 | "/* 82 | * There once was a man 83 | * with a very nice cat 84 | * the cat wore a top hat 85 | * it looked super dapper 86 | */ 87 | 88 | ", 89 | BlockComment::new("/*\n", "*/", None) 90 | .with_per_line("*") 91 | .set_trailing_lines(2) 92 | .comment(EX_TEXT) 93 | ) 94 | } 95 | 96 | #[test] 97 | fn test_comment_html() { 98 | assert_eq!( 99 | "", 105 | BlockComment::new("", None).comment(EX_TEXT) 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "[0-9]+.[0-9]+.[0-9]+" 6 | - "[0-9]+.[0-9]+.[0-9]+-[a-z0-9]+" 7 | 8 | jobs: 9 | create-release: 10 | name: create-release 11 | runs-on: ubuntu-latest 12 | env: 13 | LICENSURE_VERSION: "" 14 | outputs: 15 | upload_url: ${{ steps.release.outputs.upload_url }} 16 | licensure_version: ${{ env.LICENSURE_VERSION }} 17 | steps: 18 | - name: Get the release version from the tag 19 | shell: bash 20 | if: env.LICENSURE_VERSION == '' 21 | run: | 22 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 23 | echo "LICENSURE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 24 | echo "version is: ${{ env.LICENSURE_VERSION }}" 25 | - name: Create GitHub release 26 | id: release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ env.LICENSURE_VERSION }} 32 | release_name: ${{ env.LICENSURE_VERSION }} 33 | 34 | build-release: 35 | name: build-release 36 | needs: ["create-release"] 37 | runs-on: ${{ matrix.os }} 38 | env: 39 | CARGO: cargo 40 | TARGET_FLAGS: "--target ${{ matrix.target }}" 41 | TARGET_DIR: ./target/${{ matrix.target }} 42 | strategy: 43 | # Just because one OS isn't building doesn't mean we shouldn't build the 44 | # rest. 45 | fail-fast: false 46 | matrix: 47 | build: [linux, linux-arm, macos] 48 | include: 49 | - build: linux 50 | os: ubuntu-latest 51 | rust: stable 52 | target: x86_64-unknown-linux-gnu 53 | - build: linux-arm 54 | os: ubuntu-latest 55 | rust: stable 56 | target: arm-unknown-linux-gnueabihf 57 | - build: macos 58 | os: macos-latest 59 | rust: stable 60 | target: x86_64-apple-darwin 61 | 62 | steps: 63 | - name: Checkout repository 64 | uses: actions/checkout@v3 65 | 66 | - name: Install Rust 67 | uses: dtolnay/rust-toolchain@master 68 | with: 69 | toolchain: ${{ matrix.rust }} 70 | target: ${{ matrix.target }} 71 | 72 | - name: Use Cross 73 | shell: bash 74 | run: | 75 | cargo install cross 76 | echo "CARGO=cross" >> $GITHUB_ENV 77 | 78 | - name: Show command used for Cargo 79 | run: | 80 | echo "cargo command is: ${{ env.CARGO }}" 81 | echo "target flag is: ${{ env.TARGET_FLAGS }}" 82 | echo "target dir is: ${{ env.TARGET_DIR }}" 83 | 84 | - name: Build release binary 85 | run: ${{ env.CARGO }} build --verbose --release ${{ env.TARGET_FLAGS }} 86 | 87 | - name: Strip release binary (linux and macos) 88 | if: matrix.build == 'linux' || matrix.build == 'macos' 89 | run: strip "target/${{ matrix.target }}/release/licensure" 90 | 91 | - name: Strip release binary (arm) 92 | if: matrix.build == 'linux-arm' 93 | run: | 94 | docker run --rm -v \ 95 | "$PWD/target:/target:Z" \ 96 | rustembedded/cross:arm-unknown-linux-gnueabihf \ 97 | arm-linux-gnueabihf-strip \ 98 | /target/arm-unknown-linux-gnueabihf/release/licensure 99 | 100 | - name: Build archive 101 | shell: bash 102 | run: | 103 | staging="licensure-${{ needs.create-release.outputs.licensure_version }}-${{ matrix.target }}" 104 | mkdir -p "$staging/complete" 105 | 106 | cp {README.md,LICENSE} "$staging/" 107 | cp "target/${{ matrix.target }}/release/licensure" "$staging/" 108 | 109 | tar czvf "$staging.tar.gz" "$staging" 110 | echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV 111 | 112 | - name: Upload release archive 113 | uses: actions/upload-release-asset@v1.0.2 114 | env: 115 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 116 | with: 117 | upload_url: ${{ needs.create-release.outputs.upload_url }} 118 | asset_path: ${{ env.ASSET }} 119 | asset_name: ${{ env.ASSET }} 120 | asset_content_type: application/octet-stream 121 | -------------------------------------------------------------------------------- /src/config/default.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2024 Mathew Robinson 2 | // 3 | // This program is free software: you can redistribute it and/or modify it under 4 | // the terms of the GNU General Public License as published by the Free Software 5 | // Foundation, version 3. 6 | // 7 | // This program is distributed in the hope that it will be useful, but WITHOUT 8 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License along with 12 | // this program. If not, see . 13 | // 14 | // Simply contains the default YAML config for generation and consumption 15 | pub const DEFAULT_CONFIG: &str = r#" 16 | # Regexes which if matched by a file path will always be excluded from 17 | # getting a license header 18 | excludes: 19 | - \.gitignore 20 | - .*lock 21 | - \.git/.* 22 | - \.licensure\.yml 23 | - README.* 24 | - LICENSE.* 25 | - .*\.(md|rst|txt) 26 | # Definition of the licenses used on this project and to what files 27 | # they should apply. 28 | # 29 | # No default license configuration is provided. This section must be 30 | # configured by the user. 31 | # 32 | # Make sure to delete the [] below when you add your configs. 33 | licenses: [] 34 | # Either a regex or the string "any" to determine to what files this 35 | # license should apply. It is common for projects to have files 36 | # under multiple licenses or with multiple copyright holders. This 37 | # provides the ability to automatically license files correctly 38 | # based on their file paths. 39 | # 40 | # If "any" is provided all files will match this license. 41 | # - files: any 42 | # 43 | # The license identifier, a list of common identifiers can be 44 | # found at: https://spdx.org/licenses/ but existence of the ident 45 | # in this list it is not enforced unless auto_template is set to 46 | # true. 47 | # ident: MIT 48 | # 49 | # A list of authors who hold copyright over these files 50 | # authors: 51 | # Provide either your full name or company name for copyright purposes 52 | # - name: Your Name Here 53 | # Optionally provide email for copyright purposes 54 | # email: you@yourdomain.com 55 | # 56 | # The template that will be rendered to generate the header before 57 | # comment characters are applied. Available variables are: 58 | # - [year]: substituted with the current year. 59 | # - [name of author]: Substituted with name of the author and email 60 | # if provided. If email is provided the output appears as Full 61 | # Name . If multiple authors are provided the 62 | # list is concatenated together with commas. 63 | # template: | 64 | # Copyright [year] [name of author]. All rights reserved. Use of 65 | # this source code is governed by the [ident] license that can be 66 | # found in the LICENSE file. 67 | # 68 | # If auto_template is true then template is ignored and the SPDX 69 | # API will be queried with the ident value to automatically 70 | # determine the license header template. auto_template works best 71 | # with licenses that have a standardLicenseHeader field defined in 72 | # their license info JSON, if it is not then we will use the full 73 | # licenseText to generate the header which works fine for short 74 | # licenses like MIT but can be quite lengthy for other licenses 75 | # like BSD-4-Clause. The above default template is valid for most 76 | # licenses and is recommended for MIT, and BSD licenses. Common 77 | # licenses that work well with the auto_template feature are GPL 78 | # variants, and the Apache 2.0 license. 79 | # 80 | # Important Note: this means the ident must be a valid SPDX identifier 81 | # auto_template: true 82 | # 83 | # If true try to detect the text wrapping of the template, and unwrap it 84 | # unwrap_text: false 85 | 86 | # Define type of comment characters to apply based on file extensions. 87 | comments: 88 | # The extensions (or singular extension) field defines which file 89 | # extensions to apply the commenter to. 90 | - extensions: 91 | - js 92 | - rs 93 | - go 94 | # The commenter field defines the kind of commenter to 95 | # generate. There are two types of commenters: line and block. 96 | # 97 | # This demonstrates a line commenter configuration. A line 98 | # commenter type will apply the comment_char to the beginning of 99 | # each line in the license header. It will then apply a number of 100 | # empty newlines to the end of the header equal to trailing_lines. 101 | # 102 | # If trailing_lines is omitted it is assumed to be 0. 103 | commenter: 104 | type: line 105 | comment_char: "//" 106 | trailing_lines: 0 107 | - extensions: 108 | - css 109 | - cpp 110 | - c 111 | # This demonstrates a block commenter configuration. A block 112 | # commenter type will add start_block_char as the first character 113 | # in the license header and add end_block_char as the last character 114 | # in the license header. If per_line_char is provided each line of 115 | # the header between the block start and end characters will be 116 | # line commented with the per_line_char 117 | # 118 | # trailing_lines works the same for both block and line commenter 119 | # types 120 | commenter: 121 | type: block 122 | start_block_char: "/*\n" 123 | end_block_char: "*/" 124 | per_line_char: "*" 125 | trailing_lines: 0 126 | # In this case extension is singular and a single string extension is provided. 127 | - extension: html 128 | commenter: 129 | type: block 130 | start_block_char: "" 132 | - extensions: 133 | - el 134 | - lisp 135 | commenter: 136 | type: line 137 | comment_char: ";;;" 138 | trailing_lines: 0 139 | # The extension string "any" is special and so will match any file 140 | # extensions. Commenter configurations are always checked in the 141 | # order they are defined, so if any is used it should be the last 142 | # commenter configuration or else it will override all others. 143 | # 144 | # In this configuration if we can't match the file extension we fall 145 | # back to the popular '#' line comment used in most scripting 146 | # languages. 147 | - extension: any 148 | commenter: 149 | type: line 150 | comment_char: '#' 151 | trailing_lines: 0 152 | 153 | "#; 154 | -------------------------------------------------------------------------------- /src/config/comment.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | // Copyright (C) 2024 Mathew Robinson 4 | // 5 | // This program is free software: you can redistribute it and/or modify it under 6 | // the terms of the GNU General Public License as published by the Free Software 7 | // Foundation, version 3. 8 | // 9 | // This program is distributed in the hope that it will be useful, but WITHOUT 10 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU General Public License along with 14 | // this program. If not, see . 15 | // 16 | use serde::Deserialize; 17 | 18 | use crate::comments::BlockComment; 19 | use crate::comments::Comment; 20 | use crate::comments::LineComment; 21 | 22 | use super::RegexList; 23 | 24 | fn def_trailing_lines() -> usize { 25 | 0 26 | } 27 | 28 | pub fn get_filetype(filename: &str) -> &str { 29 | // Get just the filename component of the given filename (which is really a path) 30 | let path_filename = Path::new(filename) 31 | .file_name() 32 | .unwrap_or_default() 33 | // We should always be able to go to_str here because we created the os_str from a &str 34 | .to_str() 35 | .unwrap(); 36 | 37 | // If there's no "." in the filename, return no extension 38 | if !path_filename.contains('.') { 39 | return ""; 40 | } 41 | 42 | let iter = path_filename.split('.'); 43 | iter.last().unwrap_or_default() 44 | } 45 | 46 | #[derive(Clone, Deserialize, Debug)] 47 | #[serde(tag = "type")] 48 | pub enum Commenter { 49 | #[serde(alias = "block")] 50 | Block { 51 | start_block_char: String, 52 | end_block_char: String, 53 | per_line_char: Option, 54 | #[serde(default = "def_trailing_lines")] 55 | trailing_lines: usize, 56 | }, 57 | #[serde(alias = "line")] 58 | Line { 59 | comment_char: String, 60 | #[serde(default = "def_trailing_lines")] 61 | trailing_lines: usize, 62 | }, 63 | } 64 | 65 | #[derive(Clone, Deserialize, Debug)] 66 | #[serde(untagged)] 67 | enum FileType { 68 | Single(String), 69 | List(Vec), 70 | } 71 | 72 | impl FileType { 73 | fn matches(&self, ft: &str) -> bool { 74 | match self { 75 | FileType::Single(ext) => ext == "any" || ext == ft, 76 | FileType::List(extensions) => extensions.iter().any(|ext| ext == ft), 77 | } 78 | } 79 | } 80 | 81 | #[derive(Clone, Deserialize, Debug)] 82 | pub struct Config { 83 | #[serde(alias = "extensions")] 84 | extension: FileType, 85 | #[serde(default)] 86 | files: Option, 87 | columns: Option, 88 | commenter: Commenter, 89 | } 90 | 91 | impl Config { 92 | pub fn matches(&self, file_type: &str, filename: &str) -> bool { 93 | if self.extension.matches(file_type) { 94 | if let Some(files) = &self.files { 95 | files.is_match(filename) 96 | } else { 97 | true 98 | } 99 | } else { 100 | false 101 | } 102 | } 103 | 104 | pub fn commenter(&self) -> Box { 105 | match &self.commenter { 106 | Commenter::Line { 107 | comment_char, 108 | trailing_lines, 109 | } => Box::new( 110 | LineComment::new(comment_char.as_str(), self.get_columns()) 111 | .set_trailing_lines(*trailing_lines), 112 | ), 113 | Commenter::Block { 114 | start_block_char, 115 | end_block_char, 116 | per_line_char, 117 | trailing_lines, 118 | } => { 119 | let mut bc = BlockComment::new( 120 | start_block_char.as_str(), 121 | end_block_char.as_str(), 122 | self.get_columns(), 123 | ) 124 | .set_trailing_lines(*trailing_lines); 125 | 126 | if let Some(ch) = per_line_char { 127 | bc = bc.with_per_line(ch.as_str()); 128 | } 129 | 130 | Box::new(bc) 131 | } 132 | } 133 | } 134 | 135 | pub fn get_columns(&self) -> Option { 136 | self.columns 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | pub mod tests { 142 | use super::*; 143 | 144 | #[test] 145 | fn test_get_filetype() { 146 | assert_eq!("py", get_filetype("test.py")); 147 | assert_eq!("htaccess", get_filetype(".htaccess")); 148 | assert_eq!("htaccess", get_filetype("/foo/bar/.htaccess")); 149 | assert_eq!("htaccess", get_filetype("./foo/.bar/.htaccess")); 150 | assert_eq!("htaccess", get_filetype("./.htaccess")); 151 | assert_eq!("html", get_filetype("index.html")); 152 | assert_eq!("html", get_filetype("/foo/bar/index.html")); 153 | assert_eq!("html", get_filetype("./foo/.bar/index.html")); 154 | assert_eq!("html", get_filetype("./index.html")); 155 | assert_eq!("", get_filetype("NONE")); 156 | assert_eq!("", get_filetype("/foo/bar/NONE")); 157 | assert_eq!("", get_filetype("./foo/.bar/NONE")); 158 | assert_eq!("", get_filetype("./NONE")); 159 | } 160 | 161 | static COMMENT_CONFIG_PY: &str = r##"columns: 80 162 | extensions: 163 | - py 164 | commenter: 165 | type: line 166 | comment_char: "#""##; 167 | 168 | static COMMENT_CONFIG_PY_EXAMPLE: &str = r##"columns: 80 169 | extensions: 170 | - py 171 | files: 172 | - example/.* 173 | commenter: 174 | type: line 175 | comment_char: "#""##; 176 | #[test] 177 | fn test_matches() { 178 | let config_py: Config = 179 | serde_yaml::from_str(COMMENT_CONFIG_PY).expect("Parsing static config"); 180 | let config_py_example: Config = 181 | serde_yaml::from_str(COMMENT_CONFIG_PY_EXAMPLE).expect("Parsing static config"); 182 | 183 | let file = "example/foo.py"; 184 | assert!(config_py.matches(get_filetype(file), file)); 185 | assert!(config_py_example.matches(get_filetype(file), file)); 186 | 187 | let file = "example/foo.c"; 188 | assert!(!config_py.matches(get_filetype(file), file)); 189 | assert!(!config_py_example.matches(get_filetype(file), file)); 190 | 191 | let file = "another_dir/foo.py"; 192 | assert!(config_py.matches(get_filetype(file), file)); 193 | assert!(!config_py_example.matches(get_filetype(file), file)); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2024 Mathew Robinson 2 | // 3 | // This program is free software: you can redistribute it and/or modify it under 4 | // the terms of the GNU General Public License as published by the Free Software 5 | // Foundation, version 3. 6 | // 7 | // This program is distributed in the hope that it will be useful, but WITHOUT 8 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 | // 11 | // You should have received a copy of the GNU General Public License along with 12 | // this program. If not, see . 13 | // 14 | use std::env; 15 | use std::fs::File; 16 | use std::io; 17 | use std::path::PathBuf; 18 | use std::process; 19 | 20 | use regex::Regex; 21 | use regex::RegexSet; 22 | use serde::Deserialize; 23 | 24 | pub use default::DEFAULT_CONFIG; 25 | 26 | use crate::comments::Comment; 27 | use crate::config::comment::Config as CommentConfig; 28 | use crate::config::comment::get_filetype; 29 | use crate::config::license::Config as LicenseConfig; 30 | use crate::template::Template; 31 | 32 | mod comment; 33 | mod default; 34 | mod license; 35 | 36 | fn default_off() -> bool { 37 | false 38 | } 39 | 40 | #[derive(Deserialize, Debug)] 41 | pub struct Config { 42 | #[serde(default = "default_off")] 43 | pub change_in_place: bool, 44 | 45 | pub excludes: RegexList, 46 | pub licenses: LicenseConfigList, 47 | pub comments: CommentConfigList, 48 | } 49 | 50 | impl Config { 51 | pub fn add_exclude(&mut self, pat: &str) { 52 | self.excludes.add_exclude(pat); 53 | } 54 | } 55 | 56 | impl Default for Config { 57 | fn default() -> Self { 58 | serde_yaml::from_str(DEFAULT_CONFIG).expect("The default config is invalid?") 59 | } 60 | } 61 | 62 | #[derive(Deserialize, Debug, Default, Clone)] 63 | #[serde(from = "Vec")] 64 | pub struct RegexList { 65 | regex: RegexSet, 66 | } 67 | 68 | impl RegexList { 69 | pub fn is_match(&self, s: &str) -> bool { 70 | self.regex.is_match(s) 71 | } 72 | 73 | pub fn add_exclude(&mut self, pat: &str) { 74 | let mut old_pats = Vec::from(self.regex.patterns()); 75 | let mut new_pats = vec![pat.to_string()]; 76 | new_pats.append(&mut old_pats); 77 | self.regex = match RegexSet::new(&new_pats) { 78 | Ok(r) => r, 79 | Err(e) => { 80 | println!("Failed to compile exclude pattern: {}", e); 81 | process::exit(1); 82 | } 83 | }; 84 | } 85 | } 86 | 87 | impl From> for RegexList { 88 | fn from(rgxs: Vec) -> RegexList { 89 | RegexList { 90 | regex: match RegexSet::new(rgxs) { 91 | Ok(r) => r, 92 | Err(e) => { 93 | println!("Failed to compile exclude pattern: {}", e); 94 | process::exit(1); 95 | } 96 | }, 97 | } 98 | } 99 | } 100 | 101 | #[derive(Deserialize, Debug)] 102 | #[serde(from = "Vec")] 103 | pub struct CommentConfigList { 104 | cfgs: Vec, 105 | } 106 | 107 | impl From> for CommentConfigList { 108 | fn from(cfgs: Vec) -> CommentConfigList { 109 | CommentConfigList { cfgs } 110 | } 111 | } 112 | 113 | impl CommentConfigList { 114 | /// Get the commenter for the given filename or None if no specific commenter available 115 | pub fn get_commenter(&self, filename: &str) -> Option> { 116 | let file_type = get_filetype(filename); 117 | 118 | for c in &self.cfgs { 119 | if c.matches(file_type, filename) { 120 | return Some(c.commenter()); 121 | } 122 | } 123 | None 124 | } 125 | } 126 | 127 | #[derive(Deserialize, Debug)] 128 | #[serde(from = "Vec")] 129 | pub struct LicenseConfigList { 130 | cfgs: Vec, 131 | } 132 | 133 | impl LicenseConfigList { 134 | pub fn get_template(&mut self, filename: &str) -> Option