├── .dockerignore
├── .editorconfig
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── SUPPORT.md
├── dependabot.yml
├── git-rewrite-author.png
├── labels.yml
└── workflows
│ ├── build.yml
│ ├── codeql.yml
│ └── labels.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.md
├── cmd
└── main.go
├── docker-bake.hcl
├── go.mod
├── go.sum
├── hack
├── lint.Dockerfile
└── vendor.Dockerfile
└── internal
├── app
├── app.go
├── config.go
├── list.go
└── rewrite.go
├── git
├── config.go
├── filter-branch.go
├── log.go
└── repo.go
├── logging
└── logger.go
├── model
└── cli.go
└── utl
└── string.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | /bin
2 | /dist
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs.
2 | # More information at http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_size = 2
9 | indent_style = space
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [*.go]
18 | indent_style = tab
19 |
20 | [*.json]
21 | insert_final_newline = false
22 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @crazy-max
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: crazy-max
2 | custom: https://www.paypal.me/crazyws
3 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support [](https://isitmaintained.com/project/crazy-max/git-rewrite-author)
2 |
3 | ## Reporting an issue
4 |
5 | First search for [existing issues](https://github.com/crazy-max/git-rewrite-author/issues?utf8=%E2%9C%93&q=). If it did not help you, [create a new issue](https://github.com/crazy-max/git-rewrite-author/issues/new) based on this template :
6 |
7 | ```
8 | ### Behaviour
9 |
10 | #### Steps to reproduce this issue
11 |
12 | 1.
13 | 2.
14 | 3.
15 |
16 | #### Expected behaviour
17 |
18 | > Tell me what should happen
19 |
20 | #### Actual behaviour
21 |
22 | > Tell me what happens instead
23 |
24 | ### Configuration
25 |
26 | **Git version (ex. 2.13.0)** :
27 |
28 | **Operating system (ex. Windows 10 Pro 64 bits)** :
29 |
30 | **git-rewrite-author version (ex. 1.0.0)** :
31 | ```
32 |
33 | ## Closure policy
34 |
35 | * Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines.
36 | * Issues that go a week without a response from original poster are subject to closure at my discretion.
37 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gomod"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "08:00"
8 | timezone: "Europe/Paris"
9 | labels:
10 | - ":game_die: dependencies"
11 | - ":robot: bot"
12 | - package-ecosystem: "github-actions"
13 | directory: "/"
14 | schedule:
15 | interval: "daily"
16 | time: "08:00"
17 | timezone: "Europe/Paris"
18 | labels:
19 | - ":game_die: dependencies"
20 | - ":robot: bot"
21 |
--------------------------------------------------------------------------------
/.github/git-rewrite-author.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crazy-max/git-rewrite-author/ba020df2e6f47aca8bcb698aa59f184da46653f8/.github/git-rewrite-author.png
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | ## more info https://github.com/crazy-max/ghaction-github-labeler
2 | - # automerge
3 | name: ":bell: automerge"
4 | color: "8f4fbc"
5 | description: ""
6 | - # bot
7 | name: ":robot: bot"
8 | color: "69cde9"
9 | description: ""
10 | - # bug
11 | name: ":bug: bug"
12 | color: "b60205"
13 | description: ""
14 | - # dependencies
15 | name: ":game_die: dependencies"
16 | color: "0366d6"
17 | description: ""
18 | - # documentation
19 | name: ":memo: documentation"
20 | color: "c5def5"
21 | description: ""
22 | - # duplicate
23 | name: ":busts_in_silhouette: duplicate"
24 | color: "cccccc"
25 | description: ""
26 | - # enhancement
27 | name: ":sparkles: enhancement"
28 | color: "0054ca"
29 | description: ""
30 | - # feature request
31 | name: ":bulb: feature request"
32 | color: "0e8a16"
33 | description: ""
34 | - # feedback
35 | name: ":mega: feedback"
36 | color: "03a9f4"
37 | description: ""
38 | - # future maybe
39 | name: ":rocket: future maybe"
40 | color: "fef2c0"
41 | description: ""
42 | - # good first issue
43 | name: ":hatching_chick: good first issue"
44 | color: "7057ff"
45 | description: ""
46 | - # help wanted
47 | name: ":pray: help wanted"
48 | color: "4caf50"
49 | description: ""
50 | - # invalid
51 | name: ":no_entry_sign: invalid"
52 | color: "e6e6e6"
53 | description: ""
54 | - # investigate
55 | name: ":mag: investigate"
56 | color: "e6625b"
57 | description: ""
58 | - # needs more info
59 | name: ":thinking: needs more info"
60 | color: "795548"
61 | description: ""
62 | - # pinned
63 | name: ":pushpin: pinned"
64 | color: "28008e"
65 | description: ""
66 | - # question
67 | name: ":question: question"
68 | color: "3f51b5"
69 | description: ""
70 | - # sponsor
71 | name: ":sparkling_heart: sponsor"
72 | color: "fedbf0"
73 | description: ""
74 | - # stale
75 | name: ":skull: stale"
76 | color: "237da0"
77 | description: ""
78 | - # upstream
79 | name: ":eyes: upstream"
80 | color: "fbca04"
81 | description: ""
82 | - # wontfix
83 | name: ":coffin: wontfix"
84 | color: "ffffff"
85 | description: ""
86 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | concurrency:
4 | group: build-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'master'
11 | tags:
12 | - '*'
13 | pull_request:
14 |
15 | jobs:
16 | validate:
17 | runs-on: ubuntu-latest
18 | steps:
19 | -
20 | name: Checkout
21 | uses: actions/checkout@v3
22 | -
23 | name: Set up Docker Buildx
24 | uses: docker/setup-buildx-action@v2
25 | -
26 | name: Validate
27 | uses: docker/bake-action@v2
28 | with:
29 | targets: validate
30 |
31 | build:
32 | runs-on: ubuntu-latest
33 | steps:
34 | -
35 | name: Checkout
36 | uses: actions/checkout@v3
37 | with:
38 | fetch-depth: 0
39 | -
40 | name: Set up QEMU
41 | uses: docker/setup-qemu-action@v2
42 | -
43 | name: Set up Docker Buildx
44 | uses: docker/setup-buildx-action@v2
45 | -
46 | name: Build artifacts
47 | uses: docker/bake-action@v2
48 | with:
49 | targets: artifact-all
50 | -
51 | name: Move artifacts
52 | run: |
53 | mv ./dist/**/* ./dist/
54 | -
55 | name: Upload artifacts
56 | uses: actions/upload-artifact@v3
57 | with:
58 | name: git-rewrite-author
59 | path: ./dist/*
60 | if-no-files-found: error
61 | -
62 | name: GitHub Release
63 | uses: softprops/action-gh-release@v1
64 | if: startsWith(github.ref, 'refs/tags/')
65 | with:
66 | draft: true
67 | files: |
68 | dist/*.tar.gz
69 | dist/*.zip
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: codeql
2 |
3 | concurrency:
4 | group: codeql-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'master'
11 | tags:
12 | - '*'
13 | pull_request:
14 | branches:
15 | - 'master'
16 | schedule:
17 | - cron: '0 12 * * 6'
18 |
19 | jobs:
20 | codeql:
21 | runs-on: ubuntu-latest
22 | steps:
23 | -
24 | name: Checkout
25 | uses: actions/checkout@v3
26 | with:
27 | fetch-depth: 2
28 | -
29 | name: Checkout HEAD on PR
30 | if: ${{ github.event_name == 'pull_request' }}
31 | run: |
32 | git checkout HEAD^2
33 | -
34 | name: Initialize CodeQL
35 | uses: github/codeql-action/init@v2
36 | with:
37 | languages: go
38 | -
39 | name: Autobuild
40 | uses: github/codeql-action/autobuild@v2
41 | -
42 | name: Perform CodeQL Analysis
43 | uses: github/codeql-action/analyze@v2
44 |
--------------------------------------------------------------------------------
/.github/workflows/labels.yml:
--------------------------------------------------------------------------------
1 | name: labels
2 |
3 | concurrency:
4 | group: labels-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | on:
8 | push:
9 | branches:
10 | - 'master'
11 | paths:
12 | - '.github/labels.yml'
13 | - '.github/workflows/labels.yml'
14 |
15 | jobs:
16 | labeler:
17 | runs-on: ubuntu-latest
18 | steps:
19 | -
20 | name: Checkout
21 | uses: actions/checkout@v3
22 | -
23 | name: Run Labeler
24 | uses: crazy-max/ghaction-github-labeler@v4
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin
2 | /dist
3 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 10m
3 |
4 | linters:
5 | enable:
6 | - deadcode
7 | - depguard
8 | - gofmt
9 | - goimports
10 | - revive
11 | - govet
12 | - importas
13 | - ineffassign
14 | - misspell
15 | - typecheck
16 | - varcheck
17 | - errname
18 | - makezero
19 | - whitespace
20 | disable-all: true
21 |
22 | linters-settings:
23 | depguard:
24 | list-type: blacklist
25 | include-go-root: true
26 | packages:
27 | # The io/ioutil package has been deprecated.
28 | # https://go.dev/doc/go1.16#ioutil
29 | - io/ioutil
30 | importas:
31 | no-unaliased: true
32 |
33 | issues:
34 | exclude-rules:
35 | - linters:
36 | - revive
37 | text: "stutters"
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.4.0 (2022/07/16)
4 |
5 | * Go 1.18 and add container dev workflow (#55)
6 | * Bump github.com/alecthomas/kong from 0.2.9 to 0.6.1 (#12 #13 #22 #25 #32 #37 #54)
7 | * Bump github.com/rs/zerolog from 1.18.0 to 1.27.0 (#9 #17 #26 #31 #35 #52)
8 |
9 | ## 1.3.0 (2020/05/21)
10 |
11 | * Avoid git filter-branch delay
12 | * Fix filter branch command (#1)
13 | * Switch to kong command-line parser
14 | * Go 1.13
15 | * Update deps
16 |
17 | ## 1.2.0 (2019/10/01)
18 |
19 | * Switch to GitHub Actions
20 | * Review project structure
21 | * Use GOPROXY
22 | * Go 1.12.10
23 |
24 | ## 1.1.2 (2018/09/06)
25 |
26 | * Go 1.11
27 | * Use [go mod](https://golang.org/cmd/go/#hdr-Module_maintenance) instead of `dep`
28 |
29 | ## 1.1.1 (2017/11/14)
30 |
31 | * Typo in rewrite example
32 |
33 | ## 1.1.0 (2017/11/14)
34 |
35 | * Optimization of history rewriting for a list of authors/committers
36 | * Add debug flag
37 |
38 | ## 1.0.0 (2017/11/14)
39 |
40 | * Initial version
41 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | ARG GO_VERSION="1.18"
4 | ARG GORELEASER_XX_VERSION="1.2.5"
5 | ARG ALPINE_VERSION="3.16"
6 |
7 | FROM --platform=$BUILDPLATFORM crazymax/goreleaser-xx:${GORELEASER_XX_VERSION} AS goreleaser-xx
8 | FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS base
9 | ENV CGO_ENABLED=0
10 | COPY --from=goreleaser-xx / /
11 | RUN apk add --no-cache file git
12 | WORKDIR /src
13 |
14 | FROM base AS vendored
15 | RUN --mount=type=bind,source=.,target=/src,rw \
16 | --mount=type=cache,target=/go/pkg/mod \
17 | go mod tidy && go mod download
18 |
19 | FROM vendored AS build
20 | ARG TARGETPLATFORM
21 | RUN --mount=type=bind,target=. \
22 | --mount=type=cache,target=/root/.cache \
23 | --mount=target=/go/pkg/mod,type=cache \
24 | goreleaser-xx --debug \
25 | --name "git-rewrite-author" \
26 | --dist "/out" \
27 | --main="./cmd" \
28 | --flags="-trimpath" \
29 | --ldflags="-s -w -X 'main.version={{.Version}}'" \
30 | --files="CHANGELOG.md" \
31 | --files="LICENSE" \
32 | --files="README.md"
33 |
34 | FROM scratch AS artifact
35 | COPY --from=build /out/*.tar.gz /
36 | COPY --from=build /out/*.zip /
37 |
38 | FROM scratch AS binary
39 | COPY --from=build /usr/local/bin/git-rewrite-author* /
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017-2022 CrazyMax
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## :warning: Abandoned project
13 |
14 | This project is not maintained anymore and is abandoned. Feel free to fork and
15 | make your own changes if needed.
16 |
17 | ## About
18 |
19 | **git-rewrite-author** is a CLI application written in [Go](https://golang.org/)
20 | to rewrite one or several authors / committers history of a [Git](https://git-scm.com/)
21 | repository with ease. It was inspired by [this post on Github](https://web.archive.org/web/20180604120659/https://help.github.com/articles/changing-author-info/).
22 |
23 | ___
24 |
25 | * [Requirements](#requirements)
26 | * [Download](#download)
27 | * [Installation](#installation)
28 | * [Usage](#usage)
29 | * [Build](#build)
30 | * [Contributing](#contributing)
31 | * [License](#license)
32 |
33 | ## Requirements
34 |
35 | You must have [Git](https://git-scm.com/) installed on your system and create a
36 | fresh, bare clone of your repository:
37 |
38 | ```console
39 | $ cd /tmp
40 | $ git clone --bare https://github.com/user/repo.git
41 | $ cd /tmp/repo.git
42 | ```
43 |
44 | ## Download
45 |
46 | You can download the application matching your platform on the
47 | [**releases page**](https://github.com/crazy-max/git-rewrite-author/releases/latest).
48 |
49 | ## Installation
50 |
51 | Place the executable in your Git repository. It is best to place it in your
52 | `PATH` so that you can use it anywhere in your system and also use it with the
53 | Git syntax `git rewrite-author`.
54 |
55 | ## Usage
56 |
57 | ```
58 | Usage: git-rewrite-author
59 |
60 | Rewrite authors history of a Git repository with ease. More info:
61 | https://github.com/crazy-max/git-rewrite-author
62 |
63 | Flags:
64 | --help Show context-sensitive help.
65 | --version
66 | --repo="." Git repository path.
67 | --log-level="info" Set log level.
68 | --log-caller Add file:line of the caller to log output.
69 |
70 | Commands:
71 | config-get
72 | config-set
73 | list
74 | rewrite
75 | rewrite-list
76 |
77 | Run "git-rewrite-author --help" for more information on a command.
78 | ```
79 |
80 | You probably want to know the list of authors/committers for a repository
81 | before rewritting history:
82 |
83 | ```console
84 | $ git-rewrite-author list --repo /tmp/repo.git
85 | ohcrap
86 | GitHub
87 | root
88 | ```
89 |
90 | Then you can rewrite a single author/committer:
91 |
92 | ```console
93 | $ git-rewrite-author rewrite "ohcrap@bad.com" "John Smith " --repo /tmp/repo.git
94 |
95 | Following authors/committers will be rewritten:
96 | - ohcrap@bad.com => John Smith John Smith
122 | - ohcrap@bad.com => Good Sir
123 |
124 | Rewrite 4b03c46d8f085f56014e5bee1e5597de86554139 (31/31) (22 seconds passed, remaining 0 predicted)
125 | Ref 'refs/heads/master' was rewritten
126 | Ref 'refs/remotes/origin/master' was rewritten
127 | Ref 'refs/tags/0.15.1-1' was rewritten
128 | Ref 'refs/tags/0.15.2-2' was rewritten
129 | Ref 'refs/tags/0.15.310-3' was rewritten
130 | Ref 'refs/tags/0.16.9-4' was rewritten
131 | Ref 'refs/tags/0.17.13-5' was rewritten
132 | Ref 'refs/tags/0.17.19-6' was rewritten
133 | Ref 'refs/tags/0.18.14-7' was rewritten
134 | Ref 'refs/tags/0.18.23-8' was rewritten
135 | Ref 'refs/tags/0.18.23-9' was rewritten
136 | Ref 'refs/tags/0.18.36-10' was rewritten
137 | Ref 'refs/tags/0.19.48-11' was rewritten
138 | Ref 'refs/tags/0.19.70-12' was rewritten
139 | ```
140 |
141 | Here the `authors.json` JSON file looks like this:
142 |
143 | ```json
144 | [
145 | {
146 | "old": [ "root@localhost", "noreply@github.com" ],
147 | "correct_name": "John Smith",
148 | "correct_mail": "john.smith@domain.com"
149 | },
150 | {
151 | "old": [ "ohcrap@bad.com" ],
152 | "correct_name": "Good Sir",
153 | "correct_mail": "goodsir@users.noreply.github.com"
154 | }
155 | ]
156 | ```
157 |
158 | Now confirm everything suits to you:
159 |
160 | ```console
161 | $ git-rewrite-author list --repo /tmp/repo.git
162 | Good Sir
163 | John Smith
164 | ```
165 |
166 | Review the new Git history for errors and push the corrected history to Git:
167 |
168 | ```console
169 | $ git push --force --all
170 | ```
171 |
172 | ## Build
173 |
174 | ```shell
175 | git clone https://github.com/crazy-max/git-rewrite-author.git git-rewrite-author
176 | cd git-rewrite-author
177 |
178 | # validate (lint, vendors)
179 | docker buildx bake validate
180 |
181 | # build binary in ./bin
182 | docker buildx bake
183 |
184 | # create artifacts for all supported platforms in ./dist
185 | docker buildx bake artifact-all
186 | ```
187 |
188 | ## Contributing
189 |
190 | Want to contribute? Awesome! The most basic way to show your support is to star the project, or to raise issues. You
191 | can also support this project by [**becoming a sponsor on GitHub**](https://github.com/sponsors/crazy-max) or by making
192 | a [Paypal donation](https://www.paypal.me/crazyws) to ensure this journey continues indefinitely!
193 |
194 | Thanks again for your support, it is much appreciated! :pray:
195 |
196 | ## License
197 |
198 | MIT. See `LICENSE` for more details.
199 | Icon credit to [ual Pharm](https://www.shareicon.net/author/ual-pharm).
200 |
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/alecthomas/kong"
10 | "github.com/crazy-max/git-rewrite-author/internal/app"
11 | "github.com/crazy-max/git-rewrite-author/internal/logging"
12 | "github.com/crazy-max/git-rewrite-author/internal/model"
13 | "github.com/rs/zerolog/log"
14 | )
15 |
16 | var (
17 | gra *app.GitRewriteAuthor
18 | cli model.Cli
19 | version = "dev"
20 | )
21 |
22 | func main() {
23 | var err error
24 |
25 | // Parse command line
26 | kctx := kong.Parse(&cli,
27 | kong.Name("git-rewrite-author"),
28 | kong.Description(`Rewrite authors history of a Git repository with ease. More info: https://github.com/crazy-max/git-rewrite-author`),
29 | kong.UsageOnError(),
30 | kong.Vars{
31 | "version": fmt.Sprintf("%s", version),
32 | },
33 | kong.ConfigureHelp(kong.HelpOptions{
34 | Compact: true,
35 | Summary: true,
36 | }))
37 |
38 | // Logger
39 | logging.Configure(cli)
40 |
41 | // Init
42 | if gra, err = app.New(cli); err != nil {
43 | log.Fatal().Err(err).Msg("Cannot initialize")
44 | }
45 |
46 | // Handle os signals
47 | channel := make(chan os.Signal, 1)
48 | signal.Notify(channel, os.Interrupt, syscall.SIGTERM)
49 | go func() {
50 | sig := <-channel
51 | log.Warn().Msgf("Caught signal %v", sig)
52 | os.Exit(0)
53 | }()
54 |
55 | switch kctx.Command() {
56 | case "config-get":
57 | gra.ConfigGet()
58 | case "config-set ":
59 | gra.ConfigSet()
60 | case "list":
61 | gra.List()
62 | case "rewrite ":
63 | gra.RewriteOne()
64 | case "rewrite-list ":
65 | gra.RewriteList()
66 | default:
67 | log.Fatal().Err(err).Msg("Unknown command")
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | variable "GO_VERSION" {
2 | default = "1.18"
3 | }
4 |
5 | target "_common" {
6 | args = {
7 | GO_VERSION = GO_VERSION
8 | BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1
9 | }
10 | }
11 |
12 | group "default" {
13 | targets = ["binary"]
14 | }
15 |
16 | target "binary" {
17 | inherits = ["_common"]
18 | target = "binary"
19 | output = ["./bin"]
20 | }
21 |
22 | target "artifact" {
23 | inherits = ["_common"]
24 | target = "artifact"
25 | output = ["./dist"]
26 | }
27 |
28 | target "artifact-all" {
29 | inherits = ["artifact"]
30 | platforms = [
31 | "darwin/amd64",
32 | "darwin/arm64",
33 | "freebsd/amd64",
34 | "freebsd/386",
35 | "linux/386",
36 | "linux/amd64",
37 | "linux/arm/v5",
38 | "linux/arm/v6",
39 | "linux/arm/v7",
40 | "linux/arm64",
41 | "windows/386",
42 | "windows/amd64",
43 | "windows/arm64"
44 | ]
45 | }
46 |
47 | target "vendor" {
48 | inherits = ["_common"]
49 | dockerfile = "./hack/vendor.Dockerfile"
50 | target = "update"
51 | output = ["."]
52 | }
53 |
54 | target "gomod-outdated" {
55 | inherits = ["_common"]
56 | dockerfile = "./hack/vendor.Dockerfile"
57 | target = "outdated"
58 | no-cache-filter = ["outdated"]
59 | output = ["type=cacheonly"]
60 | }
61 |
62 | group "validate" {
63 | targets = ["lint", "vendor-validate"]
64 | }
65 |
66 | target "lint" {
67 | inherits = ["_common"]
68 | dockerfile = "./hack/lint.Dockerfile"
69 | target = "lint"
70 | output = ["type=cacheonly"]
71 | }
72 |
73 | target "vendor-validate" {
74 | inherits = ["_common"]
75 | dockerfile = "./hack/vendor.Dockerfile"
76 | target = "validate"
77 | output = ["type=cacheonly"]
78 | }
79 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/crazy-max/git-rewrite-author
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/alecthomas/kong v0.6.1
7 | github.com/pkg/errors v0.9.1
8 | github.com/rs/zerolog v1.27.0
9 | )
10 |
11 | require (
12 | github.com/mattn/go-colorable v0.1.12 // indirect
13 | github.com/mattn/go-isatty v0.0.14 // indirect
14 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA=
2 | github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I=
3 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
4 | github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
5 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
10 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
11 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
12 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
13 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
19 | github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
20 | github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
22 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
23 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
24 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
25 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
26 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
30 |
--------------------------------------------------------------------------------
/hack/lint.Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | ARG GO_VERSION="1.18"
4 | ARG GOLANGCI_LINT_VERSION="v1.45"
5 |
6 | FROM golang:${GO_VERSION}-alpine AS base
7 | ENV GOFLAGS="-buildvcs=false"
8 | RUN apk add --no-cache gcc linux-headers musl-dev
9 | WORKDIR /src
10 |
11 | FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
12 | FROM base AS lint
13 | RUN --mount=type=bind,target=. \
14 | --mount=type=cache,target=/root/.cache \
15 | --mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
16 | golangci-lint run ./...
17 |
--------------------------------------------------------------------------------
/hack/vendor.Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | ARG GO_VERSION="1.18"
4 | ARG GOMOD_OUTDATED_VERSION="v0.8.0"
5 |
6 | FROM golang:${GO_VERSION}-alpine AS base
7 | RUN apk add --no-cache git linux-headers musl-dev
8 | WORKDIR /src
9 |
10 | FROM base AS vendored
11 | RUN --mount=type=bind,target=.,rw \
12 | --mount=type=cache,target=/go/pkg/mod <&2 'ERROR: Vendor result differs. Please vendor your package with "docker buildx bake vendor"'
31 | echo "$diff"
32 | exit 1
33 | fi
34 | EOT
35 |
36 | FROM psampaz/go-mod-outdated:${GOMOD_OUTDATED_VERSION} AS go-mod-outdated
37 | FROM base AS outdated
38 | RUN --mount=type=bind,target=. \
39 | --mount=type=cache,target=/go/pkg/mod \
40 | --mount=from=go-mod-outdated,source=/home/go-mod-outdated,target=/usr/bin/go-mod-outdated \
41 | go list -mod=readonly -u -m -json all | go-mod-outdated -update -direct
42 |
--------------------------------------------------------------------------------
/internal/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/crazy-max/git-rewrite-author/internal/git"
5 | "github.com/crazy-max/git-rewrite-author/internal/model"
6 | )
7 |
8 | // GitRewriteAuthor represents an active git-rewrite-author object
9 | type GitRewriteAuthor struct {
10 | cli model.Cli
11 | repo *git.Repo
12 | }
13 |
14 | // New creates new git-rewrite-author instance
15 | func New(cli model.Cli) (*GitRewriteAuthor, error) {
16 | repo, err := git.Open(cli.Repo)
17 | return &GitRewriteAuthor{
18 | cli: cli,
19 | repo: repo,
20 | }, err
21 | }
22 |
--------------------------------------------------------------------------------
/internal/app/config.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/rs/zerolog/log"
5 | )
6 |
7 | // ConfigGet gets current user name and email from Git config
8 | func (gra *GitRewriteAuthor) ConfigGet() {
9 | gra.repo.ReloadConfig()
10 |
11 | if name, found, err := gra.repo.Get("user.name"); found {
12 | log.Info().Msgf("user.name: %s", name)
13 | } else if err != nil {
14 | log.Fatal().Err(err).Msgf("Cannot retrieve user.email in Git config")
15 | } else {
16 | log.Warn().Msg("user.name key not found in Git config")
17 | }
18 |
19 | if email, found, err := gra.repo.Get("user.email"); found {
20 | log.Info().Msgf("user.email: %s", email)
21 | } else if err != nil {
22 | log.Fatal().Err(err).Msgf("Cannot retrieve user.email in Git config")
23 | } else {
24 | log.Warn().Msg("user.email key not found in Git config")
25 | }
26 | }
27 |
28 | // ConfigSet sets user name and email to Git config
29 | func (gra *GitRewriteAuthor) ConfigSet() {
30 | if err := gra.repo.Set("user.name", gra.cli.ConfigSet.Name); err != nil {
31 | log.Fatal().Err(err).Msg("Cannot set user.name")
32 | }
33 | if err := gra.repo.Set("user.email", gra.cli.ConfigSet.Email); err != nil {
34 | log.Fatal().Err(err).Msg("Cannot set user.email")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/internal/app/list.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/rs/zerolog/log"
5 | )
6 |
7 | // List displays all authors/committers
8 | func (gra *GitRewriteAuthor) List() {
9 | logs, err := gra.repo.Logs()
10 | if err != nil {
11 | log.Fatal().Err(err).Msg("Cannot fetch git logs")
12 | }
13 |
14 | log.Debug().Msg("Seeking authors...")
15 | for _, author := range logs.GetAuthors() {
16 | log.Info().Msgf("%s <%s>", author.Name, author.Email)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/internal/app/rewrite.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "html/template"
7 | "os"
8 |
9 | "github.com/crazy-max/git-rewrite-author/internal/utl"
10 | "github.com/rs/zerolog/log"
11 | )
12 |
13 | type rewriteAuthor struct {
14 | Old []string `json:"old"`
15 | CorrectName string `json:"correct_name"`
16 | CorrectMail string `json:"correct_mail"`
17 | }
18 |
19 | // Rewrite rewrites an author/committer in Git history
20 | func (gra *GitRewriteAuthor) RewriteOne() {
21 | correctName, correctMail, err := utl.ParseAddress(gra.cli.Rewrite.Correct)
22 | if err != nil {
23 | log.Fatal().Err(err).Msg("Cannot parse new Git name and email")
24 | }
25 |
26 | gra.rewrite([]*rewriteAuthor{
27 | {
28 | Old: []string{gra.cli.Rewrite.Old},
29 | CorrectName: correctName,
30 | CorrectMail: correctMail,
31 | },
32 | })
33 | }
34 |
35 | // RewriteList rewrites a list of authors/committers in Git history
36 | func (gra *GitRewriteAuthor) RewriteList() {
37 | authorsFile, err := os.ReadFile(gra.cli.RewriteList.File)
38 | if err != nil {
39 | log.Fatal().Err(err).Msg("Cannot read authors JSON file")
40 | }
41 |
42 | var rewriteAuthors []*rewriteAuthor
43 | if err = json.Unmarshal(authorsFile, &rewriteAuthors); err != nil {
44 | log.Fatal().Err(err).Msg("Cannot unmarshal authors JSON")
45 | }
46 |
47 | gra.rewrite(rewriteAuthors)
48 | }
49 |
50 | func (gra *GitRewriteAuthor) rewrite(rewriteAuthors []*rewriteAuthor) {
51 | b, _ := json.MarshalIndent(rewriteAuthors, "", " ")
52 | log.Debug().Msg(string(b))
53 |
54 | tpl, err := template.New("rewrite").Parse(`{{range $i, $rewriteAuthor := .}}
55 | OLD_EMAILS_{{$i}}=({{range $j, $old := .Old}}{{if $j}} {{end}}"{{$old}}"{{end}})
56 | CORRECT_NAME_{{$i}}="{{.CorrectName}}"
57 | CORRECT_EMAIL_{{$i}}="{{.CorrectMail}}"
58 | for OLD_EMAIL_{{$i}} in ${OLD_EMAILS_{{$i}}[@]}; do
59 | if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL_{{$i}}" ]; then
60 | export GIT_COMMITTER_NAME="$CORRECT_NAME_{{$i}}"
61 | export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL_{{$i}}"
62 | fi
63 | if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL_{{$i}}" ]; then
64 | export GIT_AUTHOR_NAME="$CORRECT_NAME_{{$i}}"
65 | export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL_{{$i}}"
66 | fi
67 | done
68 | {{end}}`)
69 | if err != nil {
70 | log.Fatal().Err(err).Msg("Cannot parse rewrite template")
71 | }
72 |
73 | var rewriteScript bytes.Buffer
74 | if err := tpl.Execute(&rewriteScript, rewriteAuthors); err != nil {
75 | log.Fatal().Err(err).Msg("Cannot execute rewrite template")
76 | }
77 | log.Debug().Msgf("Rewrite script: %s", rewriteScript.String())
78 |
79 | log.Info().Msg("Following authors/committers will be rewritten")
80 | for _, rewriteAuthor := range rewriteAuthors {
81 | log.Info().Msgf("%s => '%s <%s>'", rewriteAuthor.Old, rewriteAuthor.CorrectName, rewriteAuthor.CorrectMail)
82 | }
83 |
84 | err = gra.repo.FilterBranch("--env-filter", rewriteScript.String(), "--tag-name-filter", "cat", "--", "--branches", "--tags")
85 | if err != nil {
86 | log.Fatal().Err(err).Msg("Cannot rewrite authors")
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/internal/git/config.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/pkg/errors"
7 | )
8 |
9 | func (r *Repo) readConfig() error {
10 | if r.cfg != nil {
11 | return nil
12 | }
13 |
14 | cmd, stdout, stderr := r.Git("config", "-l", "-z")
15 | if err := cmd.Run(); err != nil {
16 | return errors.Wrap(err, stderr.String())
17 | }
18 | r.cfg = make(ConfigMap)
19 |
20 | for _, line := range strings.Split(stdout.String(), "\x00") {
21 | parts := strings.SplitN(line, "\n", 2)
22 | if len(parts) != 2 {
23 | continue
24 | }
25 | k := strings.TrimSpace(parts[0])
26 | v := strings.TrimSpace(parts[1])
27 | if k == "" {
28 | continue
29 | }
30 | r.cfg[k] = v
31 | }
32 |
33 | return nil
34 | }
35 |
36 | // ReloadConfig will force the config for this git repo to be lazily reloaded.
37 | func (r *Repo) ReloadConfig() {
38 | r.cfg = nil
39 | }
40 |
41 | // Get a specific config value.
42 | func (r *Repo) Get(key string) (val string, found bool, err error) {
43 | err = r.readConfig()
44 | val, found = r.cfg[key]
45 | return
46 | }
47 |
48 | // Set a config variable.
49 | func (r *Repo) Set(key, val string) error {
50 | if err := r.readConfig(); err != nil {
51 | return err
52 | }
53 |
54 | cmd, _, _ := r.Git("config", "--add", key, val)
55 | if err := cmd.Run(); err != nil {
56 | return err
57 | }
58 |
59 | r.cfg[key] = val
60 | return nil
61 | }
62 |
63 | // Find all config variables with a specific prefix.
64 | func (r *Repo) Find(prefix string) (res map[string]string, err error) {
65 | if err := r.readConfig(); err != nil {
66 | return nil, err
67 | }
68 |
69 | res = make(map[string]string)
70 | for k, v := range r.cfg {
71 | if strings.HasPrefix(k, prefix) {
72 | res[k] = v
73 | }
74 | }
75 |
76 | return res, nil
77 | }
78 |
--------------------------------------------------------------------------------
/internal/git/filter-branch.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "os"
7 | "os/exec"
8 | )
9 |
10 | func (r *Repo) FilterBranch(args ...string) (err error) {
11 | var path string
12 | if r.WorkDir == "" {
13 | path = r.GitDir
14 | } else {
15 | path = r.WorkDir
16 | }
17 |
18 | cmdArgs := []string{"filter-branch"}
19 | cmdArgs = append(cmdArgs, args...)
20 |
21 | stdout, stderr := os.Stdout, new(bytes.Buffer)
22 |
23 | cmd := exec.Command(gitCmd, cmdArgs...)
24 | cmd.Stdout, cmd.Stderr = stdout, stderr
25 | cmd.Dir = path
26 | cmd.Env = []string{
27 | "FILTER_BRANCH_SQUELCH_WARNING=1",
28 | }
29 |
30 | if err := cmd.Run(); err != nil {
31 | return errors.New(stderr.String())
32 | }
33 |
34 | return nil
35 | }
36 |
--------------------------------------------------------------------------------
/internal/git/log.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "errors"
5 | "sort"
6 | "strings"
7 | )
8 |
9 | // Log is the main struct that we use to track Git log for a repository.
10 | type Log struct {
11 | Hash string
12 | Author Author
13 | Committer Author
14 | }
15 |
16 | type Author struct {
17 | Name string
18 | Email string
19 | }
20 |
21 | type Authors []Author
22 |
23 | type Logs []Log
24 |
25 | func (r *Repo) Logs() (logs Logs, err error) {
26 | cmd, stdout, stderr := r.Git("--no-pager", "log", "--pretty=%h;%aN,%ae;%cN,%ce")
27 | if err := cmd.Run(); err != nil {
28 | return logs, errors.New(stderr.String())
29 | }
30 |
31 | for _, line := range strings.Split(stdout.String(), "\n") {
32 | parts := strings.SplitN(line, ";", 3)
33 | if len(parts) != 3 {
34 | continue
35 | }
36 | authorParts := strings.SplitN(strings.TrimSpace(parts[1]), ",", 2)
37 | if len(authorParts) != 2 {
38 | continue
39 | }
40 | committerParts := strings.SplitN(strings.TrimSpace(parts[2]), ",", 2)
41 | if len(committerParts) != 2 {
42 | continue
43 | }
44 |
45 | logs = append(logs, Log{
46 | Hash: strings.TrimSpace(parts[0]),
47 | Author: Author{
48 | Name: authorParts[0],
49 | Email: authorParts[1],
50 | },
51 | Committer: Author{
52 | Name: committerParts[0],
53 | Email: committerParts[1],
54 | },
55 | })
56 | }
57 |
58 | return logs, err
59 | }
60 |
61 | func (l Logs) GetAuthors() (authors Authors) {
62 | for _, log := range l {
63 | authors = appendUniqueAuthor(authors, log.Author)
64 | authors = appendUniqueAuthor(authors, log.Committer)
65 | }
66 | sort.Sort(authors)
67 | return authors
68 | }
69 |
70 | func appendUniqueAuthor(authors Authors, author Author) Authors {
71 | for _, anAuthor := range authors {
72 | if anAuthor.Name == author.Name && anAuthor.Email == author.Email {
73 | return authors
74 | }
75 | }
76 | return append(authors, author)
77 | }
78 |
79 | func (slice Authors) Len() int {
80 | return len(slice)
81 | }
82 |
83 | func (slice Authors) Less(i, j int) bool {
84 | switch strings.Compare(strings.ToUpper(slice[i].Name), strings.ToUpper(slice[j].Name)) {
85 | case -1:
86 | return true
87 | case 0, 1:
88 | return false
89 | default:
90 | return false
91 | }
92 | }
93 |
94 | func (slice Authors) Swap(i, j int) {
95 | slice[i], slice[j] = slice[j], slice[i]
96 | }
97 |
--------------------------------------------------------------------------------
/internal/git/repo.go:
--------------------------------------------------------------------------------
1 | package git
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "strings"
11 |
12 | "github.com/rs/zerolog/log"
13 | )
14 |
15 | // ConfigMap maps config keys to their values.
16 | type ConfigMap map[string]string
17 |
18 | // Repo is the main struct that we use to track Git repositories.
19 | type Repo struct {
20 | GitDir string
21 | WorkDir string
22 | cfg ConfigMap
23 | }
24 |
25 | var gitCmd string
26 |
27 | func init() {
28 | var err error
29 | if gitCmd, err = exec.LookPath("git"); err != nil {
30 | log.Fatal().Err(err).Msg("Cannot find git command")
31 | }
32 | }
33 |
34 | func findRepo(path string) (found bool, gitdir, workdir string, err error) {
35 | stat, err := os.Stat(path)
36 | if err != nil {
37 | return
38 | }
39 | if !stat.IsDir() {
40 | err = errors.New(path + " is not a directory")
41 | return
42 | }
43 |
44 | if strings.HasSuffix(path, ".git") {
45 | if stat, err = os.Stat(filepath.Join(path, "config")); err == nil {
46 | found = true
47 | gitdir = path
48 | workdir = ""
49 | return
50 | }
51 | }
52 |
53 | if stat, err = os.Stat(filepath.Join(path, ".git", "config")); err != nil {
54 | return
55 | }
56 |
57 | found = true
58 | gitdir = filepath.Join(path, ".git")
59 | workdir = path
60 | return
61 | }
62 |
63 | // Open the first git repository that "owns" path.
64 | func Open(path string) (repo *Repo, err error) {
65 | if path == "" {
66 | path = "."
67 | }
68 |
69 | path, err = filepath.Abs(path)
70 | basepath := path
71 | if err != nil {
72 | return
73 | }
74 |
75 | for {
76 | found, gitdir, workdir, _ := findRepo(path)
77 | if found {
78 | repo = new(Repo)
79 | repo.GitDir = gitdir
80 | repo.WorkDir = workdir
81 | return
82 | }
83 | parent := filepath.Dir(path)
84 | if parent == path {
85 | break
86 | }
87 | path = parent
88 | }
89 |
90 | return nil, fmt.Errorf("could not find a Git repository in %s or any of its parents", basepath)
91 | }
92 |
93 | // Git is a helper for creating exec.Cmd types and arranging to capture
94 | // the output and erro streams of the command into bytes.Buffers
95 | func Git(cmd string, args ...string) (res *exec.Cmd, stdout, stderr *bytes.Buffer) {
96 | cmdArgs := []string{cmd}
97 | cmdArgs = append(cmdArgs, args...)
98 | res = exec.Command(gitCmd, cmdArgs...)
99 | stdout, stderr = new(bytes.Buffer), new(bytes.Buffer)
100 | res.Stdout, res.Stderr = stdout, stderr
101 | return
102 | }
103 |
104 | // Git is a helper for making sure that the Git command runs in the proper repository.
105 | func (r *Repo) Git(cmd string, args ...string) (res *exec.Cmd, out, err *bytes.Buffer) {
106 | var path string
107 | if r.WorkDir == "" {
108 | path = r.GitDir
109 | } else {
110 | path = r.WorkDir
111 | }
112 | res, out, err = Git(cmd, args...)
113 | res.Dir = path
114 | return
115 | }
116 |
117 | // IsRaw checks to see if this is a raw repository.
118 | func (r *Repo) IsRaw() (res bool) {
119 | return r.WorkDir == ""
120 | }
121 |
122 | // Path returns the best idea of the path to the repository.
123 | // The exact value returned depends on whether this is a
124 | // raw repository or not.
125 | func (r *Repo) Path() (path string) {
126 | if r.IsRaw() {
127 | return r.GitDir
128 | }
129 | return r.WorkDir
130 | }
131 |
--------------------------------------------------------------------------------
/internal/logging/logger.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/crazy-max/git-rewrite-author/internal/model"
8 | "github.com/rs/zerolog"
9 | "github.com/rs/zerolog/log"
10 | )
11 |
12 | // Configure configures logger
13 | func Configure(cli model.Cli) {
14 | var err error
15 |
16 | ctx := zerolog.New(zerolog.ConsoleWriter{
17 | Out: os.Stdout,
18 | TimeFormat: time.RFC1123,
19 | }).With().Timestamp()
20 |
21 | if cli.LogCaller {
22 | ctx = ctx.Caller()
23 | }
24 |
25 | log.Logger = ctx.Logger()
26 |
27 | logLevel, err := zerolog.ParseLevel(cli.LogLevel)
28 | if err != nil {
29 | log.Fatal().Err(err).Msgf("Unknown log level")
30 | } else {
31 | zerolog.SetGlobalLevel(logLevel)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/internal/model/cli.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "github.com/alecthomas/kong"
4 |
5 | // Cli holds command line args, flags and cmds
6 | type Cli struct {
7 | Version kong.VersionFlag
8 | Repo string `kong:"name='repo',type:'path',default='.',help='Git repository path.'"`
9 | LogLevel string `kong:"name='log-level',default='info',help='Set log level.'"`
10 | LogCaller bool `kong:"name='log-caller',default='false',help='Add file:line of the caller to log output.'"`
11 | ConfigGet struct {
12 | } `kong:"cmd,name='config-get',help:'Get current user name and email from Git config.'"`
13 | ConfigSet struct {
14 | Name string `kong:"arg,required,name='name',help='Git username.'"`
15 | Email string `kong:"arg,required,name='email',help='Git email.'"`
16 | } `kong:"cmd,name='config-set',help:'Set user name and email to Git config.'"`
17 | List struct {
18 | } `kong:"cmd,name='list',help:'Display all authors/committers.'"`
19 | Rewrite struct {
20 | Old string `kong:"arg,required,name='old',help='Current email linked to Git author to rewrite.'"`
21 | Correct string `kong:"arg,required,name='correct',help='New Git name and email to set.'"`
22 | } `kong:"cmd,name='rewrite',help:'Rewrite an author/committer in Git history.'"`
23 | RewriteList struct {
24 | File string `kong:"arg,required,name='file',type:'path',help='Authors JSON file.'"`
25 | } `kong:"cmd,name='rewrite-list',help:'Rewrite a list of authors/committers in Git history.'"`
26 | }
27 |
--------------------------------------------------------------------------------
/internal/utl/string.go:
--------------------------------------------------------------------------------
1 | package utl
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | )
7 |
8 | // ParseAddress parses a single RFC 5322 address, e.g. "Barry Gibbs "
9 | func ParseAddress(address string) (name string, email string, err error) {
10 | re := regexp.MustCompile("(.*) <(.*)>")
11 | match := re.FindStringSubmatch(address)
12 | if match == nil || len(match) != 3 {
13 | return "", "", fmt.Errorf("cannot parse %s", address)
14 | }
15 | return match[1], match[2], nil
16 | }
17 |
--------------------------------------------------------------------------------