3P by GN⁺ | ★ favorite | 댓글 1개
  • Difftastic는 줄 단위가 아니라 코드의 구문 구조를 기준으로 파일을 비교하는 CLI diff 도구임
  • 내부적으로 tree-sitter로 코드를 파싱해, 포매팅 때문에 줄이 바뀌어도 실제로 달라진 표현식을 더 잘 구분함
  • 코드 포매터가 항목을 여러 줄로 나누거나 래퍼가 추가된 경우에도, 변경된 구조와 유지된 내부 내용을 분리해 보여줄 수 있음
  • Git 설정 안내와 설치 문서가 제공되며, 여러 프로그래밍 언어와 JSON·YAML 같은 구조화 텍스트 형식을 지원함
  • MIT 라이선스로 공개되어 다운로드, 수정, 공유가 가능하며 기존 diff 흐름에 구조적 비교를 더하는 용도로 적합함

줄이 아니라 구문 구조를 비교함

  • Difftastic는 파일을 구문 기준으로 비교하는 CLI diff 도구임
  • 일반적인 줄 단위 diff보다 코드 구조를 이해한 결과를 보여주는 데 초점을 둠
  • 코드는 tree-sitter로 파싱함
    • 줄 중심 diff에서는 바뀐 것처럼 보일 수 있는 내부 표현식도 실제 변경 여부를 구분할 수 있음
  • 코드 포매터가 코드를 여러 줄로 나누는 경우에도 실제 변경 사항을 보여줄 수 있음
  • 래퍼가 추가되면 구분자를 정확히 매칭함
    • 내부 내용이 함께 바뀌어도 추가된 래퍼를 표시할 수 있음
  • @@ -5,6 +5,7 @@ 같은 diff hunk 문법 대신, 변경 전후 파일의 실제 줄 번호를 보여줌

지원 언어와 Git 연동

  • 프로젝트 자료는 GitHub, Manual, Install에서 제공됨
  • 지원 프로그래밍 언어 예시는 다음과 같음
    • C++, C#, Clojure, Dart, Erlang, Go, Haskell, Java, JavaScript, Kotlin, Lisp, Lua, OCaml, PHP, Python, R, Ruby, Rust, Scala, TypeScript
    • 전체 목록은 매뉴얼의 지원 언어 목록에서 확인 가능함
  • 지원 파일 형식 예시는 다음과 같음
  • Git과 함께 사용할 수 있으며, 매뉴얼에 Git 설정 안내가 있음
  • Difftastic은 MIT 라이선스로 공개되어 다운로드, 수정, 공유가 가능함

댓글과 토론

Hacker News 의견들
  • 이미 아는 분도 있겠지만, 이 도구는 tree-sitter(https://tree-sitter.github.io/tree-sitter/) 위에 만들어졌고, LSP가 분석에 해준 일을 파싱에 해주는 셈임
    코드를 추상 구문 트리(AST)로 바꾸고, 그 AST를 에디터나 diff 도구 같은 클라이언트가 쓸 수 있게 하는 표준 인터페이스를 제공함
    이런 도구가 수십 개 언어를 직접 지원할 필요 없이 tree-sitter만 지원하면 tree-sitter가 지원하는 모든 언어에서 자동으로 동작할 수 있음
    새 언어를 개발한다면 tree-sitter 파서를 만들면 되고, 그러면 tree-sitter를 다루는 모든 도구가 그 언어를 지원할 수 있게 됨
    이 두 가지 큰 혁신 덕분에 이런 도구 개선이 폭발적으로 늘고 있음. 이제 에디터, diff 도구 등은 서로 같은 작업을 반복하지 않고도 수십~수백 개 언어를 지원할 수 있으니 정말 대단함

    • 전적으로 동의하며, 작년에 쓴 댓글을 다시 가져오자면 tree-sitter가 의존성 없는 구조라는 점도 강조할 만함
      참고로 우리 팀원 몇 명은 OCaml tree-sitter 바인딩을 유지보수하고, Semgrep 작업의 일부로 문법에도 자주 기여함. Semgrep은 코드 검색과, 코드 조각 자체인 질의를 AST 매처로 파싱하는 데 tree-sitter를 사용함
      린터를 만들 때는 대상 언어의 런타임을 같이 가져와야 하는 경우가 많음. 예를 들어 Python에서 내장 ast 모듈로 파서를 만들면 언어 버전과 기능을 맞춰야 해서, Python 2.7에서 실행되는 Pylint로 Python 3 코드를 파싱할 수 없음
      여러 언어를 대상으로 하면 처음 생각보다 훨씬 귀찮아짐
      tree-sitter 이전에는 최신 문법을 따라간다는 보장이 있어서 언어 내장 AST 도구를 쓰는 게 최선인 경우가 많았음
      개인적으로 tree-sitter의 천재성은 전통적인 문법 방식보다 언어 파서를 최신 상태로 유지하기 훨씬 쉽게 만들었다는 데 있음
      tree-sitter의 설계 선택을 더 알고 싶다면 Max Brunsfield의 Strange Loop 발표를 강력 추천함: https://www.youtube.com/watch?v=Jes3bD6P0To
      그 결과 difftastic 외에도 neovim, Zed, Semgrep, GitHub 코드 검색 같은 여러 새 도구가 tree-sitter 위에 만들어졌음
    • tree-sitter가 훌륭한 도구라는 데는 동의하지만, 직접 문법을 작성하는 일은 엄청나게 어려울 수 있음
      VHDL용 tree-sitter 문법과 하이라이팅 질의 세트를 작성해 봤는데, VHDL 문법을 tree-sitter로 표현하는 데 어려움이 많았음
    • 이 설명은 정확하지 않다고 봄. tree-sitter를 말한다는 식의 공통 인터페이스는 없고, 각 tree-sitter 파서는 표준 추상 구문 트리가 아니라 서로 다른 구체 구문 트리를 내보냄
      LSP는 질의 지향 컴파일러를 위한 표준 인터페이스를 사용해서 M개의 에디터와 N개의 언어 사이에 필요한 M×N 통합 문제를 실제로 해결함
      tree-sitter는 그 문제를 해결한다기보다, 에디터나 도구에서 N개의 통합을 작성하는 일을 훨씬 쉽게 만들어 줌
    • tree-sitter에서 가장 걸리는 점은 관심 있는 여러 언어에 이 접근법이 통하지 않는다는 것임
      Common Lisp는 완전한 Lisp 구현 없이는 파싱할 수 없고, Haskell 문법은 충분히 복잡해서 문법이 불완전하며, C/C++는 전처리기만 봐도 정확히 파싱하기 어렵고, Perl 파싱은 튜링 완전함
      다른 곳에서 나온 제안처럼 새 생태계에서 파서를 다시 쓰게 하지 말고, 기존 파서가 부가 출력으로 생성할 수 있는 형식을 정의하는 편이 맞아 보임
    • 영어 같은 자연어용 tree-sitter 문법을 만들어서 각 문장을 하나의 문장 단위로 라벨링할 수 있을까?
      그러면 difftastic으로 시각적 줄이 아니라 문장 기준 변경을 보여줄 수 있을 것 같음
      에세이에서 시각적 줄 기준 diff는 이상함. 보통 바뀐 문장은 시각적 줄의 중간에서 시작하기 때문임
  • cargo로 설치했다면 difftastic과 관련 도구를 업데이트하는 방법은 다음과 같음
    cargo install cargo-update
    cargo install-update --list
    cargo install-update --all
    cargo로 설치할 수 있는 재미있는 Rust 프로젝트도 있음
    https://mise.jdx.dev/ mise-en-place는 https://asdf-vm.com/의 빠르고 유연한 대체품임
    https://github.com/ajeetdsouza/zoxide는 훌륭한 cd 대체 도구로, 이동한 위치를 저장해 두었다가 "z hel" 같은 부분 일치만으로 "~/projects/helloworld"로 이동할 수 있음
    https://github.com/bootandy/dustdu를 보완하는 도구로, 어떤 디렉터리가 디스크 공간을 많이 쓰는지 보여줌

    • 깔끔한 도구 세 가지를 더 들면 https://github.com/eza-community/eza는 시각적으로 더 보기 좋은 ls, https://github.com/ClementTsang/bottom은 그래프가 있는 htop, https://github.com/sharkdp/bat은 구문 강조가 있는 cat
    • zoxide를 정말 좋아함. 목록에 lsd도 추가할 만함. 더 예쁜 ls
    • mise-en-place를 설치해 봤는데, asdf가 이렇게 됐으면 했던 바로 그 모습임
    • du류 중에서는 dua-cli가 가장 마음에 듦. ncdu 복제판인 대화형 터미널 UI임
      ncdu에 라이트 모드가 없어서 찾아보다가 발견했음
    • ncdu가 단연 최고의 du 대체 도구임
  • git 같은 도구가 왜 이런 걸 기본으로 제공하지 않는지 거의 이해가 안 됨
    difft를 1년 정도 써 왔고, 가장 큰 불만은 difft가 없는 환경에서 다른 diff 도구로 돌아가기 어렵게 만든다는 점임
    의미 기반 diff 도구도 연구된 게 있는지 궁금함. 예를 들어 문법은 바뀌었지만 의미는 같은 경우를 다루는 도구 말임
    일반적인 문제로는 난해해 보이지만, 작은 DSL이나 일부 언어의 부분집합에서는 가능하거나 유용할지도 모름

    • 기본값으로는 오래된 diff를 쓰는 게 맞다고 봄
      git을 실행하려는 거의 모든 시스템에 이미 있고, 빠르고, 작고, 누구나 기본 사용법을 알고 있음
      그래도 그 기본값을 쉽게 바꿀 수 있어서 좋음
    • https://semanticdiff.com/를 만들고 있는데, 십진 리터럴을 16진수로 바꾸거나 JSON 객체 안의 키 순서를 바꾸는 것 같은 기본적인 의미 변화를 감지함
      명령줄 유틸리티는 아니고 VS Code 확장과 GitHub App임
      작동 방식과 difftastic과의 차이가 궁금하면 https://semanticdiff.com/blog/semanticdiff-vs-difftastic/를 보면 됨
    • Difftastic은 유용한 도구지만, 내 경험상 git처럼 널리 쓰이는 도구의 기본 선택지로 삼기에는 너무 느림
    • 이런 걸 하려면 diff 도구가 사실상 컴파일러가 됨
    • 지금 git에 difft를 붙여 써 보고 있는데 꽤 마음에 듦
      그래도 기본값이 되면 안 된다고 보는 이유 중 하나는 공백 차이를 숨긴다는 점임. 설정 가능한지는 아직 확인하지 않았음
  • @@ -5,6 +5,7 @@ 문법을 읽을 줄 아는지 묻고, Difftastic은 변경 전후 파일의 실제 줄 번호를 보여준다는 설명에 완전 공감함
    바로 넣고 git diff를 해 봤는데 아주 잘 동작함

    • 그 문법을 못 읽는 건가?
  • GitHub가 이런 걸 내놓지 않았다는 건 제품 혁신의 큰 공백처럼 보임
    들여쓰기만 바뀌어도 도와주는 기능이 없고, 보통 거대한 삭제와 추가로 보여줌
    GitHub의 diff 뷰어는 더 똑똑할 수 있고 그래야 함

    • GitHub는 의미 기반 diff는커녕 구문 인식도 제대로 못함
      실제로 foo.cpp.infoo.mk.in이 다르다는 것도 구분하지 못함
      어떤 foo.t든 Perl로 선언되고 고칠 방법도 없음. 10년 된 티켓도 있음
    • tree-sitter는 정확성이 아니라 에디터에서 쓰기 위한 성능에 최적화되어 있음
      실제로 TS 핵심 개발자들도 문법의 정확성에 너무 신경 쓰지 말라고 권함[1]
      GitHub나 비슷한 입장의 서비스에는 이 제약이 치명적일 수 있어 보임
      [1] https://github.com/tree-sitter/tree-sitter/issues/130#issuec...
    • GitHub에는 diff에서 공백을 무시하는 옵션이 있음
  • 이 도구로 바꿔 보려 했지만, 바뀌지 않은 부분에도 이상한 서식을 써서 너무 시끄럽게 느껴졌음
    결국 icdiff[0]로 돌아갔음
    [0]: https://github.com/jeffkaufman/icdiff

  • 예전에 Semantic Merge[1]를 발견했는데 오픈소스는 아니었음
    이건 병합은 안 하고 diff만 하지만, 적어도 오픈소스이고 diff가 훨씬 보기 좋아서 이미 기본값으로 바꿨음
    병합 기능으로 확장할 계획이 있는지 궁금함
    [1] https://docs.plasticscm.com/semanticmerge

    • GitHub README에 따르면 difftastic은 병합을 하지 않음
      AST 병합은 어려운 문제이고 difftastic이 다루지 않는다고 함
      또한 AST diff는 텍스트 diff 관점에서 손실이 있는 과정임. difftastic은 문법적으로 의미 없는 공백을 무시하지만, 병합에는 공백 추적이 필요함
    • 나도 Semantic Merge를 추천하려 했음
      큰 팀에서 C# 프로젝트의 지저분한 리팩터링을 하던 때에는 정말 구세주 같은 도구였음
  • 이전 논의들:
    https://news.ycombinator.com/item?id=27768861 (297 points | 3 years ago | 61 comments)
    https://news.ycombinator.com/item?id=32746258 (698 points | 2 years ago | 90 comments)
    https://news.ycombinator.com/item?id=30841244 (983 points | 2 years ago | 219 comments)

  • 훌륭한 도구 같지만, 큰 XML 파일 몇 개로 확인해 보니 수정된 줄은 일반 초록색, 수정된 속성은 굵은 초록색으로 보여서 시각적으로 구분하기 어려웠음
    문서에서 diff 스타일을 바꾸거나 굵은 경우에 다른 색을 지정하는 방법을 찾지 못했음
    방법을 아는지 궁금함

    • 아쉽게도 아직 색상 사용자 지정은 지원하지 않는 것 같음
      이 기능을 지지한다고 이 이슈[1]에 댓글을 달았음
      원하는 기능이 같은 범주라면 거기에 의견을 보태도 되고, 충분히 다른 기능이라고 생각하면 새 이슈를 열어도 될 듯함
      [1]: https://github.com/Wilfred/difftastic/issues/611
  • Arch Linux 패키지는 7MB인데 설치 후에는 80MB로 풀리고, difft 바이너리만 해도 78MB라는 점이 흥미로움
    LZ4 압축을 켠 ZFS 데이터셋에서는 du가 17MB라고 함
    바이너리 안에서 이렇게 잘 압축되는 무언가를 그냥 압축해 두지 않는 이유가 궁금함. RAM에서 풀면 더 빨리 로드될 수도 있어 보임

    • 압축 이야기가 나와서 궁금해 확인해 보니, btrfs zstd:1에서는 약 10MB임
      Processed 1 file, 614 regular extents (614 refs), 0 inline.
      Type Perc Disk Usage Uncompressed Referenced
      TOTAL 14% 10M 77M 77M
      none 100% 1.1M 1.1M 1.1M
      zstd 12% 9.8M 76M 76M
    • Nim 파서가 병합 불가능한 60MB짜리 C 파일이라는 댓글과 관련이 있을 듯함
      Tree-sitter는 엄청나게 많은 코드를 필요로 하는 것 같고, 그중 상당수는 중복되거나 압축 가능해 보임
    • 실행 파일은 RAM에 통째로 로드된 뒤 시작되는 게 아님
      메모리 매핑되고 실제로 사용되는 부분만 필요할 때 로드됨
      그래서 큰 바이너리를 압축하면 수요 페이징이 작동하지 않아 오히려 느려짐
      몇 년 전 280MB짜리 Windows EXE로 테스트했는데, 70MB 정도로 압축되긴 했지만 원본보다 시작에 몇 초 더 걸렸음
      네트워크 너머로 바이너리를 실행하는 경우 같은 일부 시나리오에서는 말이 될 수 있지만, 대부분은 압축하지 않은 바이너리가 더 빨리 시작됨