Repository: orhun/git-cliff (language: python)

Issue:
Improve Monorepository Support
### Is there an existing issue or pull request for this?

- [X] I have searched the existing issues and pull requests

### Feature description

Instead of having to execute commands, like so:

`git cliff --include-path "projects/rawkode.academy/zulip/**/*" --repository ../../../`

I'd like to instead have a `cliff.yaml` nested throughout my monorepository that allows me to segment my monorepository.

### Desired solution

Adding a `cliff.yaml` to a directory should automatically set `--include-paths` to that directory and lower.

git-cliff should always use the Git root as `--repository`

### Alternatives considered

N/A

### Additional context

_No response_


Discussion / hints:
Thanks for opening your first issue at **git-cliff**! Be sure to follow the issue template! ⛰️


Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 836e2ab692..415fb19152 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -917,6 +917,7 @@ dependencies = [
  "indicatif",
  "lazy_static",
  "log",
+ "pathdiff",
  "pprof",
  "pretty_assertions",
  "rand",
@@ -1944,9 +1945,9 @@ dependencies = [
 
 [[package]]
 name = "pathdiff"
-version = "0.2.1"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
 
 [[package]]
 name = "percent-encoding"
diff --git a/git-cliff-core/src/error.rs b/git-cliff-core/src/error.rs
index 255d99c20d..0485209dc7 100644
--- a/git-cliff-core/src/error.rs
+++ b/git-cliff-core/src/error.rs
@@ -101,6 +101,9 @@ pub enum Error {
 	/// Error that may occur while handling location of directories.
 	#[error("Directory error: `{0}`")]
 	DirsError(String),
+	/// Error that may occur while constructing patterns.
+	#[error("Pattern error: `{0}`")]
+	PatternError(#[from] glob::PatternError),
 }
 
 /// Result type of the core library.
diff --git a/git-cliff-core/src/repo.rs b/git-cliff-core/src/repo.rs
index 869addbc6c..a8cc93299a 100644
--- a/git-cliff-core/src/repo.rs
+++ b/git-cliff-core/src/repo.rs
@@ -39,7 +39,7 @@ const CHANGED_FILES_CACHE: &str = "changed_files_cache";
 pub struct Repository {
 	inner:                    GitRepository,
 	/// Repository path.
-	pub path:                 PathBuf,
+	path:                     PathBuf,
 	/// Cache path for the changed files of the commits.
 	changed_files_cache_path: PathBuf,
 }
@@ -74,6 +74,15 @@ impl Repository {
 		}
 	}
 
+	/// Returns the path of the repository.
+	pub fn path(&self) -> PathBuf {
+		let mut path = self.inner.path().to_path_buf();
+		if path.ends_with(".git") {
+			path.pop();
+		}
+		path
+	}
+
 	/// Parses and returns the commits.
 	///
 	/// Sorts the commits by their time.
diff --git a/git-cliff/Cargo.toml b/git-cliff/Cargo.toml
index 73d78d5c2d..cf2d286750 100644
--- a/git-cliff/Cargo.toml
+++ b/git-cliff/Cargo.toml
@@ -57,6 +57,7 @@ env_logger = "=0.10.2"
 pprof = { version = "0.14", optional = true }
 rand = { version = "0.8.4", optional = true }
 url.workspace = true
+pathdiff = "0.2.3"
 
 [dependencies.git-cliff-core]
 version = "2.7.0" # managed by release.sh
diff --git a/git-cliff/src/lib.rs b/git-cliff/src/lib.rs
index 7bd819cf70..f7e458c59e 100644
--- a/git-cliff/src/lib.rs
+++ b/git-cliff/src/lib.rs
@@ -40,6 +40,7 @@ use git_cliff_core::{
 	DEFAULT_CONFIG,
 	IGNORE_FILE,
 };
+use glob::Pattern;
 use std::env;
 use std::fs::{
 	self,
@@ -210,9 +211,26 @@ fn process_repository<'a>(
 			}
 		}
 	}
+
+	// Include only the current directory if not running from the root repository
+	let mut include_path = args.include_path.clone();
+	if let Some(mut path_diff) =
+		pathdiff::diff_paths(env::current_dir()?, repository.path())
+	{
+		if include_path.is_none() && path_diff != Path::new("") {
+			info!(
+				"Including changes from the current directory: {:?}",
+				path_diff.display()
+			);
+			path_diff.extend(["**", "*"]);
+			include_path =
+				Some(vec![Pattern::new(path_diff.to_string_lossy().as_ref())?]);
+		}
+	}
+
 	let mut commits = repository.commits(
 		commit_range.as_deref(),
-		args.include_path.clone(),
+		include_path,
 		args.exclude_path.clone(),
 	)?;
 	if let Some(commit_limit_value) = config.git.limit_commits {
@@ -250,7 +268,7 @@ fn process_repository<'a>(
 		let commit = Commit::from(git_commit);
 		let commit_id = commit.id.to_string();
 		release.commits.push(commit);
-		release.repository = Some(repository.path.to_string_lossy().into_owned());
+		release.repository = Some(repository.path().to_string_lossy().into_owned());
 		if let Some(tag) = tags.get(&commit_id) {
 			release.version = Some(tag.name.to_string());
 			release.message.clone_from(&tag.message);
@@ -387,7 +405,7 @@ pub fn run(mut args: Opt) -> Result<()> {
 		}
 	}
 
-	// Parse the configuration file.
+	// Set path for the configuration file.
 	let mut path = args.config.clone();
 	if !path.exists() {
 		if let Some(config_path) = dirs::config_dir()
@@ -397,6 +415,7 @@ pub fn run(mut args: Opt) -> Result<()> {
 		}
 	}
 
+	// Parse the configuration file.
 	// Load the default configuration if necessary.
 	let mut config = if let Ok((config, name)) = builtin_config {
 		info!("Using built-in configuration file: {name}");
@@ -405,6 +424,20 @@ pub fn run(mut args: Opt) -> Result<()> {
 		Config::parse(&path)?
 	} else if let Some(contents) = Config::read_from_manifest()? {
 		Config::parse_from_str(&contents)?
+	} else if let Some(discovered_path) =
+		env::current_dir()?.ancestors().find_map(|dir| {
+			let path = dir.join(DEFAULT_CONFIG);
+			if path.is_file() {
+				Some(path)
+			} else {
+				None
+			}
+		}) {
+		info!(
+			"Using configuration from parent directory: {}",
+			discovered_path.display()
+		);
+		Config::parse(&discovered_path)?
 	} else {
 		if !args.context {
 			warn!(
diff --git a/website/docs/usage/monorepos.md b/website/docs/usage/monorepos.md
index 18e8a504b0..d60c2e8a93 100644
--- a/website/docs/usage/monorepos.md
+++ b/website/docs/usage/monorepos.md
@@ -4,16 +4,18 @@ sidebar_position: 5
 
 # Monorepos
 
-You can generate a changelog scoped to a specific directory via `--include-path` and `--exclude-path`.
-
-This requires changing the current working directory to the target folder. The included/excluded paths must be relative to the repository's root.
+You can generate a changelog scoped to a specific directory by just switching to that directory:
 
 ```bash
 cd packages/some_library
-git cliff --include-path "packages/some_library/**/*" --repository "../../"
+git cliff
 ```
 
+To include/exclude specific paths, use the `--include-path` and `--exclude-path` arguments:
+
 ```bash
 cd packages/some_library
-git cliff --include-path "packages/some_library/**/*" --repository "../../" --exclude-path ".github/*"
+git cliff --include-path "packages/some_library/**/*" --exclude-path ".github/*"
 ```
+
+These paths must be relative to the repository's root and should be a valid glob pattern.


Test patch:
diff --git a/.github/fixtures/test-monorepo-include-path/cliff.toml b/.github/fixtures/test-monorepo-include-path/cliff.toml
new file mode 100644
index 0000000000..b26308cde6
--- /dev/null
+++ b/.github/fixtures/test-monorepo-include-path/cliff.toml
@@ -0,0 +1,34 @@
+[changelog]
+# template for the changelog footer
+header = """
+# Changelog\n
+All notable changes to this project will be documented in this file.\n
+"""
+# template for the changelog body
+# https://keats.github.io/tera/docs/#introduction
+body = """
+{% if version %}\
+    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+{% else %}\
+    ## [unreleased]
+{% endif %}\
+{% for group, commits in commits | group_by(attribute="group") %}
+    ### {{ group | upper_first }}
+    {% for commit in commits %}
+        - {{ commit.message | upper_first }}\
+    {% endfor %}
+{% endfor %}\n
+"""
+# template for the changelog footer
+footer = """
+<!-- generated by git-cliff -->
+"""
+# remove the leading and trailing whitespace from the templates
+trim = true
+
+[git]
+# regex for parsing and grouping commits
+commit_parsers = [
+    { message = "^feat", group = "Features", default_scope = "app" },
+    { message = "^fix", group = "Bug Fixes", scope = "cli" },
+]
diff --git a/.github/fixtures/test-monorepo-include-path/commit.sh b/.github/fixtures/test-monorepo-include-path/commit.sh
new file mode 100755
index 0000000000..641a2f761e
--- /dev/null
+++ b/.github/fixtures/test-monorepo-include-path/commit.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -e
+
+git remote add origin https://github.com/orhun/git-cliff
+git fetch
+mv cliff.toml cliff.toml.bak
+git checkout 076feb74b4d8c8634669f57d4e2765c39490d80e
+mv cliff.toml.bak cliff.toml
diff --git a/.github/fixtures/test-monorepo-include-path/expected.md b/.github/fixtures/test-monorepo-include-path/expected.md
new file mode 100644
index 0000000000..3f7e907445
--- /dev/null
+++ b/.github/fixtures/test-monorepo-include-path/expected.md
@@ -0,0 +1,18 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased]
+
+### Bug Fixes
+
+- Include the root commit when `--latest` is used with one tag (#901)
+- Match PR and release metadata correctly (#907)
+- Fix missing commit fields in context (#837) (#920)
+- Preserve first time contributors (#925)
+
+### Features
+
+- Allow overriding the remote API URL via config (#896)
+
+<!-- generated by git-cliff -->
diff --git a/.github/workflows/test-fixtures.yml b/.github/workflows/test-fixtures.yml
index 2ad7a95257..9df1326e80 100644
--- a/.github/workflows/test-fixtures.yml
+++ b/.github/workflows/test-fixtures.yml
@@ -104,6 +104,8 @@ jobs:
             command: --tag v0.2.0
           - fixtures-name: test-custom-remote-api-url
             command: v1.4.0..v1.4.1
+          - fixtures-name: test-monorepo-include-path
+            command: v2.6.1..v2.7.0 --include-path .github/fixtures/
 
     steps:
       - name: Checkout