Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -73,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

Expand All @@ -101,7 +121,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
Expand Down
19 changes: 7 additions & 12 deletions src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Repository::discover(directory)
Expand Down Expand Up @@ -56,18 +56,13 @@ fn canonical(path: &Path) -> Result<PathBuf> {
fn default_branch(repo: &Repository) -> Result<String> {
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<Vec<String>> {
Expand Down
43 changes: 43 additions & 0 deletions tests/features/default.feature
Original file line number Diff line number Diff line change
@@ -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)
"""
1 change: 1 addition & 0 deletions tests/features/empty.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions tests/features/non-empty.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions tests/features/output.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/features/subdirectory.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions tests/features/untracked.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading