Last active
January 12, 2026 03:14
-
-
Save fsubal/4d663c9fad65724c57993d2da1873605 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.example.gitstatus; | |
| public class FileDiff { | |
| /** | |
| * @see https://git-scm.com/docs/git-status#_porcelain_format_version_1 | |
| */ | |
| private static final Pattern PORCELAIN_FORMAT_VERSION_1 = Pattern.compile("^( M| D|\?\?)\s*(.+)$"); | |
| /** | |
| * 本当はこれ以外にもあるが、これしか想定しない | |
| * | |
| * @see https://git-scm.com/docs/git-status#_porcelain_format_version_1 | |
| */ | |
| public enum DiffType { | |
| MODIFIED, | |
| DELETED, | |
| ADDED | |
| public static DiffType fromString(String diffType) { | |
| return switch (diffType) { | |
| case " M" -> MODIFIED; | |
| case " D" -> DELETED; | |
| case "??" -> ADDED; | |
| default -> throw new IllegalArgumentException("Invalid diffType: " + diffType); | |
| }; | |
| } | |
| } | |
| public static class InvalidLineException extends Exception { | |
| public InvalidLineException(String message, Throwable cause) { | |
| super(message, cause); | |
| } | |
| } | |
| public static FileDiff of(String line, Directory directory) throws InvalidLineException { | |
| try { | |
| var matcher = PORCELAIN_FORMAT_VERSION_1.matcher(line); | |
| if (!matcher.matches()) { | |
| throw new InvalidLineException("Invalid line: " + line, null); | |
| } | |
| var filePath = directory.getPath().resolve(Paths.get(matcher.group(2)).normalize()); | |
| var status = Status.fromString(matcher.group(1)); | |
| return new FileDiff(filePath, status); | |
| } catch (IOException e) { | |
| throw new InvalidLineException("Failed to resolve path: " + matcher.group(2), e); | |
| } | |
| } | |
| private final Path filePath; | |
| private final DiffType diffType; | |
| private FileDiff(Path filePath, DiffType diffType) { | |
| this.filePath = filePath; | |
| this.diffType = diffType; | |
| } | |
| public boolean isModified() { | |
| return diffType == DiffType.MODIFIED; | |
| } | |
| public boolean isDeleted() { | |
| return diffType == DiffType.DELETED; | |
| } | |
| public boolean isAdded() { | |
| return diffType == DiffType.ADDED; | |
| } | |
| public DiffType getDiffType() { | |
| return diffType; | |
| } | |
| public Path getFilePath() { | |
| return filePath; | |
| } | |
| public byte[] getFileContent() throws InvalidFileException { | |
| try { | |
| return Files.readAllBytes(filePath); | |
| } catch (IOException e) { | |
| throw new InvalidFileException("Failed to read file: " + filePath, e); | |
| } | |
| } | |
| } | |
| public class GitStatus { | |
| private static final REPOSITORY_ROOT = new Directory("/path/to/repo"); | |
| private final Path directory; | |
| private GitStatus(Directory directory) { | |
| this.directory = directory; | |
| } | |
| public static List<FileDiff> executeIn(Directory directory) { | |
| return new GitStatus(directory).execute(); | |
| } | |
| public static List<FileDiff> executeInRepositoryRoot() { | |
| return new GitStatus(REPOSITORY_ROOT).execute(); | |
| } | |
| public List<FileDiff> execute() { | |
| try { | |
| var process = Runtime.getRuntime().exec("git status --porcelain"); | |
| var reader = new BufferedReader(new InputStreamReader(process.getInputStream())); | |
| return reader.lines() | |
| .map(line -> FileDiff.of(line, directory)) | |
| .toList(); | |
| } catch (IOException e) { | |
| throw new RuntimeException("Failed to execute git status: " + e.getMessage(), e); | |
| } | |
| } | |
| } | |
| public class Main { | |
| public static void main(String[] args) { | |
| var gitStatus = GitStatus.executeInRepositoryRoot(); | |
| gitStatus.forEach(fileDiff -> { | |
| System.out.println(fileDiff.getFilePath()); | |
| System.out.println(fileDiff.getDiffType()); | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { resolve } from 'node:path' | |
| import { readFile } from 'node:fs/promises' | |
| import { exec } from 'node:child_process' | |
| import { promisify } from 'node:util' | |
| const execp = promisify(exec) | |
| /** | |
| * @see https://git-scm.com/docs/git-status#_porcelain_format_version_1 | |
| */ | |
| const PORCELAIN_FORMAT_VERSION_1 = /( M| D|\?\?)\s*(.+)/ | |
| /** | |
| * 本当はこれ以外にもあるが、これしか想定しない | |
| * | |
| * @see https://git-scm.com/docs/git-status#_porcelain_format_version_1 | |
| */ | |
| export const enum ChangedFileStatus { | |
| Modified = ' M', | |
| Deleted = ' D', | |
| Added = '??' | |
| } | |
| export interface ChangedFile { | |
| fullpath: string | |
| status: ChangedFileStatus | |
| content: string | |
| } | |
| type GitStatusEntry = [relativePath: string, status: ChangedFileStatus] | |
| export class GitStatus { | |
| #entries: ReadonlyMap<GitStatusEntry[0], GitStatusEntry[1]> | |
| #dirname: string | |
| constructor(entries: Iterable<GitStatusEntry>, dirname = process.cwd()) { | |
| this.#entries = new Map(entries) | |
| this.#dirname = dirname | |
| } | |
| static async porcelain(dirname = process.cwd()) { | |
| const lines = await execp('git status --porcelain').then(({ stdout }) => stdout.trim().split('\n')) | |
| const entries = lines.map<GitStatusEntry>(line => { | |
| const [, status, relativePath] = line.match(PORCELAIN_FORMAT_VERSION_1) ?? [] | |
| switch(status) { | |
| case ChangedFileStatus.Added: | |
| case ChangedFileStatus.Deleted: | |
| case ChangedFileStatus.Modified: { | |
| return [relativePath, status] | |
| } | |
| default: { | |
| throw new RangeError(`Unsupported git status: ${line}`) | |
| } | |
| } | |
| }) | |
| return new this(entries, dirname) | |
| } | |
| async *[Symbol.asyncIterator]() { | |
| for (const [relativePath, status] of this.#entries) { | |
| const fullpath = resolve(this.#dirname, relativePath) | |
| const content = await readFile(fullpath, { encoding: 'utf8' }) | |
| yield { fullpath, status, content } as ChangedFile | |
| } | |
| } | |
| } | |
| const gitStatus = await GitStatus.porcelain() | |
| for await (const changedFile of gitStatus) { | |
| console.log(changedFile.status, changedFile.fullpath, changedFile.content) | |
| } | |
| new GitStatus([ | |
| ['app/models/item.rb', ChangedFileStatus.Modified], | |
| ['spec/models/item_spec.rb', ChangedFileStatus.Added] | |
| ]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment