Skip to content

Commit 1d22641

Browse files
authored
feat: commit subcommand (#15)
This patch introduces the commit subcommand, suitable for simple use cases where a single commit is all that's necessary, and the set of files that were changed is known in advance. See the updates to the README and the help output for more.
1 parent 35271c7 commit 1d22641

File tree

4 files changed

+135
-13
lines changed

4 files changed

+135
-13
lines changed

README.md

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,60 @@ GitHub on behalf of the application.
1313

1414
## Usage
1515

16-
Currently, there is one command: `commit-headless push`. It takes a target owner/repository and
17-
remote branch name, as well as a list of commit hashes as arguments *or* a list of commit hashes *in
18-
reverse chronological order (newest first)* on standard input.
16+
There are two ways to create signed headless commits with this tool: `push` and `commit`.
17+
18+
Both of these commands take a target owner/repository (eg, `--target/-T DataDog/commit-headless`)
19+
and remote branch name (eg, `--branch bot-branch`) as required flags and expect to find a GitHub
20+
token in one of the following environment variables:
21+
22+
- HEADLESS_TOKEN
23+
- GITHUB_TOKEN
24+
- GH_TOKEN
25+
26+
In normal usage, `commit-headless` will print *only* the reference to the last commit created on the
27+
remote, allowing this to easily be captured in a script.
28+
29+
More on the specifics for each command below. See also: `commit-headless <command> --help`
30+
31+
### commit-headless push
32+
33+
In addition to the required target and branch flags, the `push` command expects a list of commit
34+
hashes as arguments *or* a list of commit hashes *in reverse chronological order (newest first)*
35+
on standard input.
1936

2037
It will iterate over the supplied commits, extract the set of changed files and commit message, then
2138
craft new *remote* commits corresponding to each local commit.
2239

2340
The remote commit will have the original commit message, with "Co-authored-by" trailer for the
24-
original commit message. This is because commits created using the GraphQL API do not support
25-
setting the author or committer (they are inferred from the token owner), so adding a
26-
"Co-authored-by" trailer allows the commits to carry attribution to the original (bot) committer.
27-
28-
In normal usage, `commit-headless` will print *only* the reference to the last commit created on the
29-
remote, allowing this to easily be captured in a script. For example output, see the later section.
41+
original commit author.
3042

3143
You can use `commit-headless push` via:
3244

33-
GH_TOKEN=xyz commit-headless push --target datadog/commit-headless --branch bot-branch-remote HASH1 HASH2 HASH3 ...
45+
commit-headless push [flags...] HASH1 HASH2 HASH3 ...
3446

3547
Or, using git log (note `--oneline`):
3648

37-
git log --oneline main.. | GH_TOKEN=xyz commit-headless push --target datadog/commit-headless --branch bot-branch-remote
49+
git log --oneline main.. | commit-headless push [flags...]
50+
51+
### commit-headless commit
52+
53+
This command is more geared for creating single commits at a time. It takes a list of files to
54+
commit changes to, and those files will either be updated/added or deleted in a single commit.
55+
56+
Note that you cannot delete a file without also adding `--force` for safety reasons.
57+
58+
Examples:
59+
60+
# Commit changes to these two files
61+
commit-headless commit [flags...] -- README.md .gitlab-ci.yml
62+
63+
# Remove a file, add another one, and commit
64+
rm file/i/do/not/want
65+
echo "hello" > hi-there.txt
66+
commit-headless commit [flags...] --force -- hi-there.txt file/i/do/not/want
67+
68+
# Commit a change with a custom message
69+
commit-headless commit [flags...] -m"ran a pipeline" -- output.txt
3870

3971
## Try it!
4072

@@ -88,13 +120,14 @@ Prerelease occurs automatically on a push to main, or can be manually triggered
88120
`release:build` job on any branch.
89121

90122
Additionally, on main, the `release:publish` job will run. This job takes the prerelease image and
91-
tags it for release.
123+
tags it for release, as well as produces a CI image with various other tools.
92124

93125
You can view all releases (and prereleases) with crane:
94126

95127
```
96128
$ crane ls registry.ddbuild.io/commit-headless-prerelease
97129
$ crane ls registry.ddbuild.io/commit-headless
130+
$ crane ls registry.ddbuild.io/commit-headless-ci-image
98131
```
99132

100133
Note that the final publish job will fail unless there was also a change to `version.go` to avoid

cmd_commit.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"strings"
11+
)
12+
13+
type CommitCmd struct {
14+
remoteFlags
15+
16+
Author string `help:"Specify an author using the standard 'A U Thor <author@example.com>' format."`
17+
Message []string `short:"m" help:"Specify a commit message. If used multiple times, values are concatenated as separate paragraphs."`
18+
Force bool `help:"Force commiting empty files. Only useful if you know you're deleting a file."`
19+
Files []string `arg:"" help:"Files to commit."`
20+
}
21+
22+
func (c *CommitCmd) Help() string {
23+
return `
24+
This command can be used to create a single commit on the remote by passing in the names of files.
25+
26+
It is expected that the paths on disk match to paths on the remote. That is, if you supply
27+
"path/to/file.txt" then the contents of that file on disk will be applied to that same file on the
28+
remote when the commit is created.
29+
30+
You can also use this to delete files by passing a path to a file that does not exist on disk. Note
31+
that for safety reasons, commit-headless will require an extra flag --force before accepting
32+
deletions. It is an error to attempt to delete a file that does not exist.
33+
34+
If you pass a path to a file that does not exist on disk without the --force flag, commit-headless
35+
will print an error and exit.
36+
37+
You can supply a commit message via --message/-m and an author via --author/-a. If unspecified,
38+
default values will be used.
39+
40+
Examples:
41+
# Commit changes to these two files
42+
commit-headless commit [flags...] -- README.md .gitlab-ci.yml
43+
44+
# Remove a file, add another one, and commit
45+
rm file/i/do/not/want
46+
echo "hello" > hi-there.txt
47+
commit-headless commit [flags...] --force -- hi-there.txt file/i/do/not/want
48+
49+
# Commit a change with a custom message
50+
commit-headless commit [flags...] -m"ran a pipeline" -- output.txt
51+
`
52+
}
53+
54+
func (c *CommitCmd) Run() error {
55+
change := Change{
56+
hash: strings.Repeat("0", 40),
57+
author: c.Author,
58+
message: strings.Join(c.Message, "\n\n"),
59+
entries: map[string][]byte{},
60+
}
61+
62+
rootfs := os.DirFS(".")
63+
64+
for _, path := range c.Files {
65+
fp, err := rootfs.Open(path)
66+
if errors.Is(err, fs.ErrNotExist) {
67+
if !c.Force {
68+
return fmt.Errorf("file %q does not exist, but --force was not set", path)
69+
}
70+
71+
change.entries[path] = []byte{}
72+
continue
73+
} else if err != nil {
74+
return fmt.Errorf("could not open file %q: %w", path, err)
75+
}
76+
77+
contents, err := io.ReadAll(fp)
78+
if err != nil {
79+
return fmt.Errorf("read %q: %w", path, err)
80+
}
81+
82+
change.entries[path] = contents
83+
}
84+
85+
owner, repository := c.Target.Owner(), c.Target.Repository()
86+
87+
return pushChanges(context.Background(), owner, repository, c.Branch, c.DryRun, change)
88+
}

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type remoteFlags struct {
4949

5050
type CLI struct {
5151
Push PushCmd `cmd:"" help:"Push local commits to the remote."`
52+
Commit CommitCmd `cmd:"" help:"Create a commit directly on the remote."`
5253
Version VersionCmd `cmd:"" help:"Print version information and exit."`
5354
}
5455

version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
package main
22

3-
const VERSION = "0.2.0"
3+
const VERSION = "0.3.0"

0 commit comments

Comments
 (0)