From 64b2968d9f890f4c73abafd6b2009dd1ce8eadca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Fri, 13 Nov 2026 19:47:14 +0100 Subject: [PATCH 1/2] Compare with origin/main by default --- README.org | 73 +++++++++++++++++++++++++++-- src/git.rs | 19 +++----- tests/features/default.feature | 43 +++++++++++++++++ tests/features/empty.feature | 1 + tests/features/non-empty.feature | 6 ++- tests/features/output.feature | 1 + tests/features/subdirectory.feature | 1 + tests/features/untracked.feature | 1 + 8 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 tests/features/default.feature diff --git a/README.org b/README.org index 3ff74d2..40436a6 100644 --- a/README.org +++ b/README.org @@ -5,8 +5,30 @@ This is a small tool that helps to list the files you have changed (added/modified/removed) in your git branch. -This can be useful when working with large repositories, as you can run tools on a specific subset of files instead of the entire repository. -It can be easily integrated into your workflow to streamline your development process. +** Motivation + +The author wrote this tool for a project with very slow tests and tools. A full run took too long for each small change. The author wanted to run the tests and tools only on the files of the current branch. + +The tool lists these files: + +- the files from the commits since the branch left ~origin/main~ +- the files with uncommitted changes +- the untracked files + +The tool only prints the list. Send the list to another tool: + +#+begin_src bash +$ git-changed -e '**/*.rs' | xargs rustfmt +#+end_src + +** Alternatives + +- ~git diff --name-only $(git merge-base origin/main HEAD)~ lists the committed and the uncommitted changes. It does not list the untracked files. Add ~git ls-files --others --exclude-standard~ for those. +- lint-staged (https://github.com/lint-staged/lint-staged) runs commands on the staged files before a commit. It needs Node.js. +- lefthook (https://github.com/evilmartians/lefthook) is a git hook manager. It gives ~{staged_files}~ and ~{push_files}~ to the commands. A ~lefthook~ command can also call ~git-changed~. +- tj-actions/changed-files (https://github.com/tj-actions/changed-files) lists the changed files in GitHub Actions. + +~git-changed~ does not run commands and does not manage hooks. It only lists the files, so you can use it in a terminal, in a hook, or in CI. ** Install with Nix @@ -55,7 +77,9 @@ $ git-changed '*.rs' | xargs rustfmt *** Default branch -Per default the script uses the ~init.defaultBranch~ if that is configured, and falls back to ~main~, you can override this setting by a command line parameter: +The tool compares your branch with ~origin/main~. It does not use your local ~main~ branch, because that branch can be old. + +To use a different base, give the ~--base~ option: #+begin_src bash $ git-changed --base development @@ -101,7 +125,48 @@ Some tools may not handle files that no longer exist. With this option, you can $ git-changed -e #+end_src -*** A more complex example +*** Examples + +Run only the specs of the branch. A new spec file is in the list before you run ~git add~: + +#+begin_src bash +$ git-changed -e 'spec/**/*_spec.rb' | xargs bundle exec rspec +#+end_src + +Lint the changed files. The ~-e~ option removes the deleted files, so the linter does not fail on a missing file: + +#+begin_src bash +$ git-changed -e -0 '**/*.py' | xargs -0 ruff check +#+end_src + +Run the tests again each time that you save a changed file. entr (https://github.com/eradman/entr) reads the list of files to watch from standard input: + +#+begin_src bash +$ git-changed -e '**/*.rs' | entr -c cargo test +#+end_src + +Open all changed files in the editor: + +#+begin_src bash +$ vim $(git-changed -e) +#+end_src + +Select one changed file with fzf (https://github.com/junegunn/fzf) and open it: + +#+begin_src bash +$ vim "$(git-changed -e | fzf)" +#+end_src + +Run a linter before each push with lefthook. The list contains all the work on the branch, also the work that you did not commit: + +#+begin_src yaml +pre-push: + commands: + rubocop: + run: git-changed -e -0 '**/*.rb' | xargs -0 bundle exec rubocop +#+end_src + +Compare with the ~development~ branch and compile each changed file: #+begin_src bash $ git-changed -0 -b development '**/*.rs' | xargs -0 -n 1 rustc diff --git a/src/git.rs b/src/git.rs index cb876d5..df5870e 100644 --- a/src/git.rs +++ b/src/git.rs @@ -11,8 +11,8 @@ use std::collections::BTreeSet; use std::path::Path; use std::path::PathBuf; -const CONFIG_PATHS: [&str; 2] = ["changed.defaultBranch", "init.defaultBranch"]; -const DEFAULT_BRANCH: &str = "main"; +const CONFIG_PATH: &str = "changed.defaultBranch"; +const DEFAULT_BRANCH: &str = "origin/main"; pub fn open_repository(directory: &Path) -> Result { Repository::discover(directory) @@ -56,18 +56,13 @@ fn canonical(path: &Path) -> Result { fn default_branch(repo: &Repository) -> Result { let config = repo.config().context("Cannot read the git config")?; - for path in CONFIG_PATHS { - match config.get_string(path) { - Ok(branch) => return Ok(branch), - Err(error) if error.code() == ErrorCode::NotFound => continue, - Err(error) => { - return Err(error) - .with_context(|| format!("Cannot read {path} from the git config")) - } + match config.get_string(CONFIG_PATH) { + Ok(branch) => Ok(branch), + Err(error) if error.code() == ErrorCode::NotFound => Ok(DEFAULT_BRANCH.to_string()), + Err(error) => { + Err(error).with_context(|| format!("Cannot read {CONFIG_PATH} from the git config")) } } - - Ok(DEFAULT_BRANCH.to_string()) } fn committed_changes(repo: &Repository, base: &str) -> Result> { diff --git a/tests/features/default.feature b/tests/features/default.feature new file mode 100644 index 0000000..01aa641 --- /dev/null +++ b/tests/features/default.feature @@ -0,0 +1,43 @@ +Feature: Compare with origin/main by default + + Background: A git repository with a remote branch + Given an empty directory + When the user executes the command: "git init --initial-branch=main" + And the user executes the command: "git config user.name 'John Doe'" + And the user executes the command: "git config user.email 'jdoe@example.com'" + And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" + + Scenario: It displays the commits that are not pushed to origin/main + When the user executes the command: "touch README.md" + And the user executes the command: "git add README.md" + And the user executes the command: "git commit --message 'Adding README'" + And the user executes the tool like: "git-changed" + Then it succeeds with the following output + """ + README.md + """ + + Scenario: It ignores an old local main branch + When the user executes the command: "git switch --create upstream" + And the user executes the command: "touch LICENSE" + And the user executes the command: "git add LICENSE" + And the user executes the command: "git commit --message 'Adding LICENSE'" + And the user executes the command: "git update-ref refs/remotes/origin/main upstream" + And the user executes the command: "git switch --create feature" + And the user executes the command: "touch README.md" + And the user executes the command: "git add README.md" + And the user executes the command: "git commit --message 'Adding README'" + And the user executes the tool like: "git-changed" + Then it succeeds with the following output + """ + README.md + """ + + Scenario: It fails if origin/main does not exist + When the user executes the command: "git update-ref -d refs/remotes/origin/main" + And the user executes the tool like: "git-changed" + Then it fails with the following error + """ + Cannot find the commit of origin/main: revspec 'origin/main' not found; class=Reference (4); code=NotFound (-3) + """ diff --git a/tests/features/empty.feature b/tests/features/empty.feature index 61bbfce..57d0cb1 100644 --- a/tests/features/empty.feature +++ b/tests/features/empty.feature @@ -6,6 +6,7 @@ Feature: Empty git repository And the user executes the command: "git config user.name 'John Doe'" And the user executes the command: "git config user.email 'jdoe@example.com'" And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" Scenario: It displays nothing When the user executes the tool like: "git-changed" diff --git a/tests/features/non-empty.feature b/tests/features/non-empty.feature index abc978c..9d84410 100644 --- a/tests/features/non-empty.feature +++ b/tests/features/non-empty.feature @@ -6,6 +6,7 @@ Feature: Non-empty git repository And the user executes the command: "git config user.name 'John Doe'" And the user executes the command: "git config user.email 'jdoe@example.com'" And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" And the user executes the command: "touch README.md" And the user executes the command: "git add README.md" @@ -31,10 +32,11 @@ Feature: Non-empty git repository README.md """ - Rule: With changes committed on the base branch + Rule: With changes pushed to the base branch - Background: With committed changes + Background: With pushed changes When the user executes the command: "git commit --message 'Adding README'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" Scenario: It displays nothing if there are no changes When the user executes the tool like: "git-changed" diff --git a/tests/features/output.feature b/tests/features/output.feature index d7fef62..3eef1a0 100644 --- a/tests/features/output.feature +++ b/tests/features/output.feature @@ -6,6 +6,7 @@ Feature: Output And the user executes the command: "git config user.name 'John Doe'" And the user executes the command: "git config user.email 'jdoe@example.com'" And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" And the user executes the command: "touch c.md a.md e.md b.md d.md" Scenario: It displays the files in sorted order diff --git a/tests/features/subdirectory.feature b/tests/features/subdirectory.feature index d8177a9..dc3ead0 100644 --- a/tests/features/subdirectory.feature +++ b/tests/features/subdirectory.feature @@ -6,6 +6,7 @@ Feature: Run the tool in a subdirectory And the user executes the command: "git config user.name 'John Doe'" And the user executes the command: "git config user.email 'jdoe@example.com'" And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" And the user executes the command: "mkdir src" And the user executes the command: "touch README.md src/main.rs" And the user executes the command: "git add README.md src/main.rs" diff --git a/tests/features/untracked.feature b/tests/features/untracked.feature index ace4476..13cfad6 100644 --- a/tests/features/untracked.feature +++ b/tests/features/untracked.feature @@ -6,6 +6,7 @@ Feature: Untracked files And the user executes the command: "git config user.name 'John Doe'" And the user executes the command: "git config user.email 'jdoe@example.com'" And the user executes the command: "git commit --allow-empty --message 'Initial commit'" + And the user executes the command: "git update-ref refs/remotes/origin/main main" And the user executes the command: "mkdir docs" And the user executes the command: "touch docs/guide.md" From 5cd04b416c6298bbba9c22c5c83a3a161e76c124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Fri, 13 Nov 2026 19:47:57 +0100 Subject: [PATCH 2/2] Remove the origin/main config from the README --- README.org | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.org b/README.org index 40436a6..b04584c 100644 --- a/README.org +++ b/README.org @@ -97,11 +97,7 @@ Or only for the current repository: $ git config changed.defaultBranch development #+end_src -The value can be any revision. If your local ~main~ is older than the remote one, the tool also lists the files that changed on ~main~ after your local copy. To compare with the remote branch, set: - -#+begin_src bash -$ git config changed.defaultBranch origin/main -#+end_src +The value can be any revision, for example a branch, a tag, or ~upstream/main~. *** Filtering files with glob pattern