├── .github └── workflows │ └── build-push.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── logo.png ├── main.go └── page.gohtml /.github/workflows/build-push.yaml: -------------------------------------------------------------------------------- 1 | name: Build docker image 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - '**' 8 | 9 | jobs: 10 | build-push: 11 | name: build-push 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout this repo 15 | uses: actions/checkout@v3 16 | - name: Set up Docker Context for Buildx 17 | shell: bash 18 | id: buildx-context 19 | run: | 20 | docker context create builders 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | with: 24 | endpoint: builders 25 | - name: Docker meta 26 | id: meta 27 | uses: docker/metadata-action@v4 28 | with: 29 | images: protolambda/forkdiff 30 | flavor: latest=true 31 | tags: | 32 | type=ref,event=branch 33 | type=ref,event=pr 34 | type=semver,pattern={{version}} 35 | - name: Login to Docker Hub 36 | uses: docker/login-action@v3 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | - name: docker-build-push 41 | id: docker_build 42 | uses: docker/build-push-action@v4 43 | with: 44 | context: . 45 | file: Dockerfile 46 | tags: ${{ steps.meta.outputs.tags }} 47 | push: true 48 | platforms: linux/amd64 49 | - name: Image digest 50 | run: echo ${{ steps.docker_build.outputs.digest }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | fork.yaml 2 | index.html 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS builder 2 | 3 | ENV GO111MODULE=on \ 4 | CGO_ENABLED=0 \ 5 | GOOS=linux \ 6 | GOARCH=amd64 7 | 8 | RUN apt-get -qq update 9 | 10 | WORKDIR /src 11 | COPY . . 12 | 13 | RUN go build \ 14 | -ldflags "-s -w -extldflags '-static'" \ 15 | -o /bin/app \ 16 | . \ 17 | && strip /bin/app 18 | 19 | FROM scratch 20 | 21 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 22 | COPY --from=builder /bin/app /forkdiff 23 | 24 | ENTRYPOINT ["/forkdiff"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 @protolambda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # forkdiff 2 | 3 | Forkdiff is a simple CLI tool to diff a git fork and its base, 4 | structure and enhance it with descriptions based on a `fork.yaml`, 5 | and output a `index.html` to show the diff. 6 | 7 | Usage: 8 | 9 | ``` 10 | -repo string 11 | path to local git repository (default ".") 12 | -fork string 13 | fork page definition (default "fork.yaml") 14 | -out string 15 | output (default "index.html") 16 | -upstream-repo string (optional) 17 | path to local git repository (default value of -repo) 18 | ``` 19 | 20 | The `fork.yaml` defines the page structure, to organize and document the diff of the fork. 21 | 22 | Example: 23 | 24 | ```yaml 25 | title: "protolambda's Greeter fork" # Define the HTML page title 26 | logo: "logo.png" 27 | footer: | # define the footer with markdown 28 | [Greeter](https://github.com/protolambda/greeter) fork overview · created with [Forkdiff](https://github.com/protolambda/forkdiff) 29 | base: 30 | name: example/greeter 31 | url: https://github.com/example/greeter 32 | ref: refs/heads/master 33 | fork: 34 | name: protolambda/greeter 35 | url: https://github.com/protolambda/greeter 36 | ref: refs/heads/optimism-history 37 | def: 38 | title: "Example Fork diff" 39 | description: | # description in markdown 40 | These are some **really** important `code` modifications in the fork. 41 | The original can be found at [`github.com/example/greeter`](https://github.com/example/greeter). 42 | And the fork at [`github.com/protolambda/greeter`](https://github.com/protolambda/greeter). 43 | globs: 44 | - "hello/world/greeter.go" # list files of which the patches should be included 45 | - "hello/util/*" # use file globs to include multiple files 46 | - "hello/util/*[!_test].go" # you can ignore things with globs too 47 | sub: 48 | - title: "" # titles are optional 49 | description: "This fork tests the modifications to `greeter.go` and utils." 50 | globs: 51 | - "hello/world/greeter.go" 52 | - "hello/util/*_test.go" 53 | - title: "modifications to hello/printer" 54 | description: "The `printer` package prints greetings" 55 | globs: 56 | - "hello/printer/*" 57 | - "hello/printer/display/**" # double start glob patterns work too, for recursive matching! 58 | ignore: 59 | - "hello/printer/testdata/**" # to group test-data with the package, but ignore the lines, use "ignore" 60 | - title: "MOTD" 61 | description: "New package that generates a message of the day (MOTD) to add to the greeting" 62 | globs: 63 | - "motd/*" 64 | # files can be ignored globally, these will be listed in a separate grayed-out section, 65 | # and do not count towards the total line count. 66 | ignore: 67 | - "*.sum" 68 | ``` 69 | 70 | ## License 71 | 72 | MIT, see [`LICENSE` file](./LICENSE). 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/protolambda/forkdiff 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/bmatcuk/doublestar v1.3.4 7 | github.com/buildkite/terminal-to-html/v3 v3.7.0 8 | github.com/go-git/go-git/v5 v5.12.0 9 | github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 10 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | dario.cat/mergo v1.0.0 // indirect 16 | github.com/Microsoft/go-winio v0.6.1 // indirect 17 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 18 | github.com/cloudflare/circl v1.3.7 // indirect 19 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 20 | github.com/emirpasic/gods v1.18.1 // indirect 21 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 22 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 23 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 24 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 25 | github.com/kevinburke/ssh_config v1.2.0 // indirect 26 | github.com/pjbgf/sha1cd v0.3.0 // indirect 27 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 28 | github.com/skeema/knownhosts v1.2.2 // indirect 29 | github.com/xanzy/ssh-agent v0.3.3 // indirect 30 | golang.org/x/crypto v0.23.0 // indirect 31 | golang.org/x/mod v0.17.0 // indirect 32 | golang.org/x/net v0.25.0 // indirect 33 | golang.org/x/sync v0.7.0 // indirect 34 | golang.org/x/sys v0.20.0 // indirect 35 | golang.org/x/tools v0.21.0 // indirect 36 | gopkg.in/warnings.v0 v0.1.2 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 5 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 6 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 7 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 8 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 10 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 13 | github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= 14 | github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= 15 | github.com/buildkite/terminal-to-html/v3 v3.7.0 h1:chdLUSpiOj/A4v3dzxyOqixXI6aw7IDA6Dk77FXsvNU= 16 | github.com/buildkite/terminal-to-html/v3 v3.7.0/go.mod h1:g0ME1XqbkBSgXR9YmlIHcJIjzaMyWW+HbsG0rPb5puo= 17 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 18 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 19 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 20 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 22 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 23 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 28 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 29 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 30 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 31 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 32 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 33 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 35 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 36 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 37 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 39 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 40 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 41 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 43 | github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM= 44 | github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 45 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 46 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 47 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 48 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 49 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 50 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 53 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 57 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 58 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 59 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 60 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 61 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 62 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 63 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 67 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 68 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 69 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 70 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 71 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 72 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 73 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 74 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 77 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 78 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 79 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 80 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 81 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 82 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 83 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 84 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 85 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 86 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 87 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 88 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 89 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 90 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 91 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= 92 | golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 93 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 94 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 95 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 96 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 99 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 100 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 101 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 102 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 103 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 104 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 105 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 106 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 107 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 108 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 110 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 118 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 119 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 125 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 126 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 127 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 128 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 129 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 130 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 131 | golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= 132 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 133 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 134 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 135 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 136 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 137 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 138 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 139 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 140 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 141 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 142 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 143 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 144 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 145 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 146 | golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= 147 | golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 148 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 152 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 153 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 154 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 155 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 157 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 158 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protolambda/forkdiff/058ea2ceb859fa3797745c8ae8ff0df9d86e0a2a/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "embed" 8 | "encoding/hex" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "github.com/bmatcuk/doublestar" 13 | t2html "github.com/buildkite/terminal-to-html/v3" 14 | "github.com/go-git/go-git/v5" 15 | "github.com/go-git/go-git/v5/plumbing" 16 | "github.com/go-git/go-git/v5/plumbing/format/diff" 17 | "github.com/go-git/go-git/v5/plumbing/object" 18 | "github.com/gomarkdown/markdown" 19 | "github.com/gomarkdown/markdown/html" 20 | "github.com/gomarkdown/markdown/parser" 21 | "golang.org/x/exp/maps" 22 | "gopkg.in/yaml.v3" 23 | "os" 24 | "sort" 25 | "strings" 26 | "text/template" 27 | ) 28 | 29 | //go:embed page.gohtml 30 | var page embed.FS 31 | 32 | func main() { 33 | repoPathStr := flag.String("repo", ".", "path to local git repository (fork)") 34 | upstreamRepoPathStr := flag.String("upstream-repo", "", "path to local git repository (upstream)") 35 | forkPagePathStr := flag.String("fork", "fork.yaml", "fork page definition") 36 | outStr := flag.String("out", "index.html", "output") 37 | flag.Parse() 38 | 39 | // Upstream repo path defaults to the same value as repoPathStr if not set 40 | if *upstreamRepoPathStr == "" { 41 | upstreamRepoPathStr = repoPathStr 42 | } 43 | 44 | must := func(err error, msg string, args ...any) { 45 | if err != nil { 46 | _, _ = fmt.Fprintf(os.Stderr, msg, args...) 47 | _, _ = fmt.Fprintf(os.Stderr, "\nerror: %v\n", err) 48 | os.Exit(1) 49 | } 50 | } 51 | pageDefinition, err := readPageYaml(*forkPagePathStr) 52 | must(err, "failed to read page definition %q", *forkPagePathStr) 53 | if pageDefinition.Def == nil { 54 | must(errors.New("no fork definition defined"), "need to root fork definition") 55 | } 56 | 57 | forkRepo, err := git.PlainOpen(*repoPathStr) 58 | must(err, "failed to open git repository %q", *repoPathStr) 59 | 60 | baseRepo, err := git.PlainOpen(*upstreamRepoPathStr) 61 | must(err, "failed to open git repository %q", *upstreamRepoPathStr) 62 | 63 | findCommit := func(rr *RefRepo, repo *git.Repository) *object.Commit { 64 | if rr.Ref != "" && rr.Hash != "" { 65 | must(errors.New("hash and ref"), "cannot use both hash and reference") 66 | } 67 | if rr.Ref == "" && rr.Hash == "" { 68 | must(errors.New("no hash and no ref"), "need either hash or reference") 69 | } 70 | if rr.Ref != "" { 71 | ref, err := repo.Reference(plumbing.ReferenceName(rr.Ref), true) 72 | must(err, "failed to find git ref %q", rr.Ref) 73 | 74 | commit, err := repo.CommitObject(ref.Hash()) 75 | must(err, "failed to open commit %s", ref.Hash()) 76 | return commit 77 | } 78 | commit, err := repo.CommitObject(plumbing.NewHash(rr.Hash)) 79 | must(err, "failed to find commit hash %s", rr.Hash) 80 | return commit 81 | } 82 | 83 | baseCommit := findCommit(&pageDefinition.Base, baseRepo) 84 | baseTree, err := baseCommit.Tree() 85 | must(err, "failed to open base git tree") 86 | 87 | forkCommit := findCommit(&pageDefinition.Fork, forkRepo) 88 | forkTree, err := forkCommit.Tree() 89 | must(err, "failed to open fork git tree") 90 | 91 | forkPatch, err := baseTree.PatchContext(context.Background(), forkTree) 92 | must(err, "failed to compute patch between base and fork") 93 | 94 | baseFiles := map[string]struct{}{} 95 | forkFiles := map[string]struct{}{} 96 | patchByName := make(map[string]diff.FilePatch, len(forkPatch.FilePatches())) 97 | for _, fp := range forkPatch.FilePatches() { 98 | from, to := fp.Files() 99 | if to != nil { 100 | patchByName[to.Path()] = fp 101 | } else if from != nil { 102 | patchByName[from.Path()] = fp 103 | } else { 104 | continue 105 | } 106 | if to != nil { 107 | forkFiles[to.Path()] = struct{}{} 108 | } 109 | if from != nil { 110 | baseFiles[from.Path()] = struct{}{} 111 | } 112 | } 113 | // remove the patches that are ignored 114 | ignored := make(map[string]diff.FilePatch) 115 | for k := range patchByName { 116 | for _, globPattern := range pageDefinition.Ignore { 117 | ok, err := doublestar.Match(globPattern, k) 118 | must(err, "failed to check %q against ignore glob pattern %q", k, globPattern) 119 | if ok { 120 | ignored[k] = patchByName[k] 121 | delete(patchByName, k) 122 | } 123 | } 124 | } 125 | remaining := make(map[string]struct{}) 126 | for k := range patchByName { 127 | remaining[k] = struct{}{} 128 | } 129 | must(pageDefinition.Def.hydrate(patchByName, remaining, 1), "failed to hydrate patch stats") 130 | if len(remaining) > 0 { 131 | remainingDef := &ForkDefinition{ 132 | Title: "Other changes", 133 | Level: 2, 134 | } 135 | remainingPaths := make([]string, 0, len(remaining)) 136 | for k := range remaining { 137 | remainingPaths = append(remainingPaths, k) 138 | } 139 | sort.Strings(remainingPaths) 140 | for _, k := range remainingPaths { 141 | remainingDef.hydratePatch(k, patchByName[k], false) 142 | } 143 | pageDefinition.Def.Sub = append(pageDefinition.Def.Sub, remainingDef) 144 | pageDefinition.Def.LinesAdded += remainingDef.LinesAdded 145 | pageDefinition.Def.LinesDeleted += remainingDef.LinesDeleted 146 | } 147 | if len(ignored) > 0 { 148 | ignoredPaths := make([]string, 0, len(ignored)) 149 | for k := range ignored { 150 | ignoredPaths = append(ignoredPaths, k) 151 | } 152 | sort.Strings(ignoredPaths) 153 | ignoredDef := &ForkDefinition{ 154 | Title: "Ignored changes", 155 | Level: 4, 156 | } 157 | for _, k := range ignoredPaths { 158 | ignoredDef.hydratePatch(k, ignored[k], true) 159 | } 160 | pageDefinition.Ignored = ignoredDef 161 | pageDefinition.Def.IgnoredLinesAdded += ignoredDef.IgnoredLinesAdded 162 | pageDefinition.Def.IgnoredLinesDeleted += ignoredDef.IgnoredLinesDeleted 163 | } 164 | 165 | templ := template.New("main") 166 | templ.Funcs(template.FuncMap{ 167 | "renderMarkdown": func(md string) string { 168 | markdownRenderer := html.NewRenderer(html.RendererOptions{ 169 | Flags: html.Smartypants | html.SmartypantsFractions | html.SmartypantsDashes | html.SmartypantsLatexDashes, 170 | Generator: "forkdiff", 171 | }) 172 | markdownParser := parser.NewWithExtensions(parser.CommonExtensions | parser.OrderedListStart) 173 | return string(markdown.ToHTML([]byte(md), markdownParser, markdownRenderer)) 174 | }, 175 | "page": func() *Page { 176 | return pageDefinition 177 | }, 178 | "existsInBase": func(path string) bool { 179 | _, ok := baseFiles[path] 180 | return ok 181 | }, 182 | "existsInFork": func(path string) bool { 183 | _, ok := forkFiles[path] 184 | return ok 185 | }, 186 | "baseFileURL": func(path string) string { 187 | return fmt.Sprintf("%s/blob/%s/%s", pageDefinition.Base.URL, baseCommit.Hash, path) 188 | }, 189 | "forkFileURL": func(path string) string { 190 | return fmt.Sprintf("%s/blob/%s/%s", pageDefinition.Fork.URL, forkCommit.Hash, path) 191 | }, 192 | "baseCommitHash": func() string { 193 | return baseCommit.Hash.String() 194 | }, 195 | "forkCommitHash": func() string { 196 | return forkCommit.Hash.String() 197 | }, 198 | "renderPatch": func(fps *FilePatchStats) (string, error) { 199 | var out bytes.Buffer 200 | enc := diff.NewUnifiedEncoder(&out, 3) 201 | enc.SetSrcPrefix(pageDefinition.Base.Name + "/") 202 | enc.SetDstPrefix(pageDefinition.Fork.Name + "/") 203 | enc.SetColor(diff.NewColorConfig()) 204 | 205 | err := enc.Encode(FilePatch{filePatch: fps.Patch}) 206 | if err != nil { 207 | return "", fmt.Errorf("") 208 | } 209 | return string(t2html.Render(out.Bytes())), nil 210 | }, 211 | "randomID": func() (string, error) { 212 | var out [12]byte 213 | if _, err := rand.Read(out[:]); err != nil { 214 | return "", err 215 | } 216 | return "id-" + hex.EncodeToString(out[:]), nil 217 | }, 218 | }) 219 | templ, err = templ.ParseFS(page, "*.gohtml") 220 | must(err, "failed to parse page template") 221 | 222 | f, err := os.OpenFile(*outStr, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o755) 223 | must(err, "failed to open output file") 224 | defer f.Close() 225 | must(templ.ExecuteTemplate(f, "main", pageDefinition), "failed to build page") 226 | } 227 | 228 | func readPageYaml(path string) (*Page, error) { 229 | f, err := os.OpenFile(path, os.O_RDONLY, 0) 230 | if err != nil { 231 | return nil, fmt.Errorf("failed to read page YAML file: %w", err) 232 | } 233 | defer f.Close() 234 | dec := yaml.NewDecoder(f) 235 | dec.KnownFields(true) 236 | var page Page 237 | if err := dec.Decode(&page); err != nil { 238 | return nil, fmt.Errorf("failed to decode page YAML file: %w", err) 239 | } 240 | return &page, nil 241 | } 242 | 243 | func countOperations(chunks []diff.Chunk, op diff.Operation) (out int) { 244 | for _, ch := range chunks { 245 | if ch.Type() == op { 246 | out += strings.Count(ch.Content(), "\n") 247 | } 248 | } 249 | return 250 | } 251 | 252 | type FilePatch struct { 253 | filePatch diff.FilePatch 254 | } 255 | 256 | var _ diff.Patch = FilePatch{} 257 | 258 | func (p FilePatch) FilePatches() []diff.FilePatch { 259 | return []diff.FilePatch{p.filePatch} 260 | } 261 | 262 | func (p FilePatch) Message() string { 263 | return "" 264 | } 265 | 266 | type RefRepo struct { 267 | Name string `yaml:"name"` 268 | Ref string `yaml:"ref,omitempty"` 269 | Hash string `yaml:"hash,omitempty"` 270 | URL string `yaml:"url"` 271 | } 272 | 273 | type Page struct { 274 | Title string `yaml:"title"` 275 | Logo string `yaml:"logo"` 276 | Footer string `yaml:"footer"` 277 | Base RefRepo `yaml:"base"` 278 | Fork RefRepo `yaml:"fork"` 279 | Def *ForkDefinition `yaml:"def"` 280 | Ignore []string `yaml:"ignore"` 281 | 282 | Ignored *ForkDefinition `yaml:"-"` 283 | } 284 | 285 | type FilePatchStats struct { 286 | Path string 287 | LinesAdded int 288 | LinesDeleted int 289 | Binary bool 290 | Patch diff.FilePatch 291 | Ignored bool 292 | } 293 | 294 | type ForkDefinition struct { 295 | Title string `yaml:"title,omitempty"` 296 | Description string `yaml:"description,omitempty"` 297 | Globs []string `yaml:"globs,omitempty"` 298 | Sub []*ForkDefinition `yaml:"sub,omitempty"` 299 | Ignore []string `yaml:"ignore,omitempty"` 300 | 301 | Files []FilePatchStats `yaml:"-"` 302 | LinesAdded int `yaml:"-"` 303 | LinesDeleted int `yaml:"-"` 304 | 305 | IgnoredFiles []FilePatchStats `yaml:"-"` 306 | IgnoredLinesAdded int `yaml:"-"` 307 | IgnoredLinesDeleted int `yaml:"-"` 308 | 309 | Level int `yaml:"-"` 310 | } 311 | 312 | func (fd *ForkDefinition) hydrate(patchByName map[string]diff.FilePatch, remaining map[string]struct{}, level int) error { 313 | fd.Level = level 314 | for i, sub := range fd.Sub { 315 | if err := sub.hydrate(patchByName, remaining, level+1); err != nil { 316 | return fmt.Errorf("sub definition %d failed to hydrate: %w", i, err) 317 | } 318 | fd.LinesAdded += sub.LinesAdded 319 | fd.LinesDeleted += sub.LinesDeleted 320 | fd.IgnoredLinesAdded += sub.IgnoredLinesAdded 321 | fd.IgnoredLinesDeleted += sub.IgnoredLinesDeleted 322 | } 323 | remainingKeys := maps.Keys(remaining) 324 | sort.Strings(remainingKeys) 325 | for _, name := range remainingKeys { 326 | _, ok := remaining[name] // we remove entries while we iterate, so we have to keep checking if things are there 327 | if !ok { 328 | fmt.Printf("not remaining anymore %q\n", name) 329 | continue 330 | } 331 | p, ok := patchByName[name] 332 | if !ok { 333 | fmt.Printf("cannot find patch %q\n", name) 334 | continue 335 | } 336 | for _, globPattern := range fd.Ignore { 337 | if ok, err := doublestar.Match(globPattern, name); err != nil { 338 | return fmt.Errorf("failed to glob match ignored-entry %q against pattern %q", name, globPattern) 339 | } else if ok { 340 | fmt.Printf("ignoring %q\n", name) 341 | delete(remaining, name) 342 | fd.hydratePatch(name, p, true) 343 | break 344 | } 345 | } 346 | for _, globPattern := range fd.Globs { 347 | if ok, err := doublestar.Match(globPattern, name); err != nil { 348 | return fmt.Errorf("failed to glob match entry %q against pattern %q", name, globPattern) 349 | } else if ok { 350 | fmt.Printf("matched %q\n", name) 351 | delete(remaining, name) 352 | fd.hydratePatch(name, p, false) 353 | break 354 | } 355 | } 356 | } 357 | return nil 358 | } 359 | 360 | func (fd *ForkDefinition) hydratePatch(name string, p diff.FilePatch, ignored bool) { 361 | stat := FilePatchStats{ 362 | Path: name, 363 | LinesAdded: countOperations(p.Chunks(), diff.Add), 364 | LinesDeleted: countOperations(p.Chunks(), diff.Delete), 365 | Binary: p.IsBinary(), 366 | Patch: p, 367 | Ignored: ignored, 368 | } 369 | if ignored { 370 | fd.IgnoredFiles = append(fd.IgnoredFiles, stat) 371 | fd.IgnoredLinesAdded += stat.LinesAdded 372 | fd.IgnoredLinesDeleted += stat.LinesDeleted 373 | } else { 374 | fd.Files = append(fd.Files, stat) 375 | fd.LinesAdded += stat.LinesAdded 376 | fd.LinesDeleted += stat.LinesDeleted 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /page.gohtml: -------------------------------------------------------------------------------- 1 | {{define "main" }} 2 | {{- /*gotype: github.com/protolambda/forkdiff.Page*/ -}} 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |{{ .Path }}
192 |
193 | {{ else }}
194 |
196 | {{ .Path }}
197 |
198 | {{ end }}
199 |