Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions handle-new-mails/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: 'Handle new mails'
description: 'Processes new mails on the Git mailing list'
author: 'Johannes Schindelin'
inputs:
config:
description: 'The GitGitGadget configuration to use (see https://github.com/gitgitgadget/gitgitgadget/blob/HEAD/lib/project-config.ts)'
required: false # not just yet...
pr-repo-token:
description: 'The access token to work on the repository that holds PRs and state'
required: true
Expand Down
3 changes: 3 additions & 0 deletions handle-pr-comment/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: 'Handle PR Comment'
description: 'Handles slash commands such as /submit and /preview'
author: 'Johannes Schindelin'
inputs:
config:
description: 'The GitGitGadget configuration to use (see https://github.com/gitgitgadget/gitgitgadget/blob/HEAD/lib/project-config.ts)'
required: false # not just yet...
pr-repo-token:
description: 'The access token to work on the repository that holds PRs and state'
required: true
Expand Down
3 changes: 3 additions & 0 deletions handle-pr-push/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: 'Handle PR Pushes'
description: 'Handles when a PR was pushed'
author: 'Johannes Schindelin'
inputs:
config:
description: 'The GitGitGadget configuration to use (see https://github.com/gitgitgadget/gitgitgadget/blob/HEAD/lib/project-config.ts)'
required: false # not just yet...
pr-repo-token:
description: 'The access token to work on the repository that holds PRs and state'
required: true
Expand Down
20 changes: 20 additions & 0 deletions initialize-git-notes/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: 'Initialize the Git notes'
description: 'Creates initial, mostly empty Git notes for GitGitGadget to store its state in.'
author: 'Johannes Schindelin'
inputs:
config:
description: 'The GitGitGadget configuration to use (see https://github.com/gitgitgadget/gitgitgadget/blob/HEAD/lib/project-config.ts)'
required: false # not just yet...
pr-repo-token:
description: 'The access token to work on the repository that holds PRs and state'
required: true
initial-user:
description: 'The user that is initially the only one allowed to use GitGitGadget in the given project'
default: ${{ github.actor }}
required: true
runs:
using: 'node20'
main: './index.js'
branding:
icon: 'git-commit'
color: 'orange'
11 changes: 11 additions & 0 deletions initialize-git-notes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
async function run() {
const { CIHelper } = await import("../dist/index.js")

const ci = new CIHelper()

await ci.setupGitHubAction({
createGitNotes: true,
})
}

run()
74 changes: 66 additions & 8 deletions lib/ci-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as core from "@actions/core";
import * as fs from "fs";
import * as os from "os";
import typia from "typia";
import * as util from "util";
import { spawnSync } from "child_process";
import addressparser from "nodemailer/lib/addressparser/index.js";
Expand All @@ -9,17 +10,17 @@ import { ILintError, LintCommit } from "./commit-lint.js";
import { commitExists, git, emptyTreeName, revParse } from "./git.js";
import { GitNotes } from "./git-notes.js";
import { GitGitGadget, IGitGitGadgetOptions } from "./gitgitgadget.js";
import { getConfig } from "./gitgitgadget-config.js";
import { GitHubGlue, IGitHubUser, IPRComment, IPRCommit, IPullRequestInfo, RequestError } from "./github-glue.js";
import { toPrettyJSON } from "./json-util.js";
import { MailArchiveGitHelper } from "./mail-archive-helper.js";
import { MailCommitMapping } from "./mail-commit-mapping.js";
import { IMailMetadata } from "./mail-metadata.js";
import { IPatchSeriesMetadata } from "./patch-series-metadata.js";
import { IConfig, getExternalConfig, setConfig } from "./project-config.js";
import { IConfig } from "./project-config.js";
import { getPullRequestKeyFromURL, pullRequestKey } from "./pullRequestKey.js";
import { ISMTPOptions } from "./send-mail.js";
import { fileURLToPath } from "url";
import defaultConfig from "./gitgitgadget-config.js";

const readFile = util.promisify(fs.readFile);
type CommentFunction = (comment: string) => Promise<void>;
Expand Down Expand Up @@ -49,17 +50,29 @@ export class CIHelper {
protected maxCommitsExceptions: string[];
protected mailingListMirror: string | undefined;

public static async getConfig(configFile?: string): Promise<IConfig> {
return configFile ? await getExternalConfig(configFile) : getConfig();
public static validateConfig = typia.createValidate<IConfig>();

protected static getConfigAsGitHubActionInput(): IConfig | undefined {
if (process.env.GITHUB_ACTIONS !== "true") return undefined;
const json = core.getInput("config");
if (!json) return undefined;
const config = JSON.parse(json) as IConfig | undefined;
const result = CIHelper.validateConfig(config);
if (result.success) return config;
throw new Error(
`Invalid config:\n- ${result.errors
.map((e) => `${e.path} (value: ${e.value}, expected: ${e.expected}): ${e.description}`)
.join("\n- ")}`,
);
}

public constructor(workDir: string = "git.git", config?: IConfig, skipUpdate?: boolean, gggConfigDir = ".") {
this.config = config !== undefined ? setConfig(config) : getConfig();
public constructor(workDir: string = "pr-repo.git", config?: IConfig, skipUpdate?: boolean, gggConfigDir = ".") {
this.config = config || CIHelper.getConfigAsGitHubActionInput() || defaultConfig;
this.gggConfigDir = gggConfigDir;
this.workDir = workDir;
this.notes = new GitNotes(workDir);
this.gggNotesUpdated = !!skipUpdate;
this.mail2commit = new MailCommitMapping(this.notes.workDir);
this.mail2commit = new MailCommitMapping(this.config, this.notes.workDir);
this.mail2CommitMapUpdated = !!skipUpdate;
this.github = new GitHubGlue(workDir, this.config.repo.owner, this.config.repo.name);
this.testing = false;
Expand All @@ -72,6 +85,7 @@ export class CIHelper {
needsMailingListMirror?: boolean;
needsUpstreamBranches?: boolean;
needsMailToCommitNotes?: boolean;
createGitNotes?: boolean;
}): Promise<void> {
// help dugite realize where `git` is...
const gitExecutable = os.type() === "Windows_NT" ? "git.exe" : "git";
Expand Down Expand Up @@ -134,6 +148,47 @@ export class CIHelper {
]) {
await git(["config", key, value], { workDir: this.workDir });
}
if (setupOptions?.createGitNotes) {
if (
setupOptions.needsMailToCommitNotes ||
setupOptions.needsUpstreamBranches ||
setupOptions.needsMailingListMirror
) {
throw new Error("`createGitNotes` cannot be combined with any other options");
}
const initialUser = core.getInput("initial-user");
console.time("verify that Git notes do not yet exist");
const existingNotes = await git(
[
"ls-remote",
"origin",
GitNotes.defaultNotesRef,
"refs/notes/mail-to-commit",
"refs/notes/commit-to-mail",
],
{
workDir: this.workDir,
},
);
if (existingNotes !== "") {
throw new Error(`Git notes already exist in ${this.workDir}:\n${existingNotes}`);
}
console.timeEnd("verify that Git notes do not yet exist");
console.time("create the initial Git notes and push them");
for (const key of ["mail-to-commit", "commit-to-mail"]) {
const notes = new GitNotes(this.workDir, `refs/notes/${key}`);
await notes.initializeWithEmptyCommit();
await notes.push(this.urlRepo, this.notesPushToken);
}
const options: IGitGitGadgetOptions = {
allowedUsers: [initialUser],
};
await this.notes.set("", options, true);
await this.notes.push(this.urlRepo, this.notesPushToken);
console.timeEnd("create the initial Git notes and push them");
return;
}

console.time("fetch Git notes");
const notesRefs = [GitNotes.defaultNotesRef];
if (setupOptions?.needsMailToCommitNotes) {
Expand Down Expand Up @@ -854,6 +909,7 @@ export class CIHelper {

try {
const gitGitGadget = await GitGitGadget.get(
this.config,
this.gggConfigDir,
this.workDir,
this.urlRepo,
Expand Down Expand Up @@ -892,7 +948,7 @@ export class CIHelper {
await addComment(
`Submitted as [${
metadata?.coverLetterMessageId
}](https://${this.config.mailrepo.host}/${this.config.mailrepo.name}/${
}](https://${this.config.mailrepo.url.replace(/\/+$/, "")}/${
metadata?.coverLetterMessageId
})\n\nTo fetch this version into \`FETCH_HEAD\`:${
code
Expand Down Expand Up @@ -1071,6 +1127,7 @@ export class CIHelper {
};

const gitGitGadget = await GitGitGadget.get(
this.config,
this.gggConfigDir,
this.workDir,
this.urlRepo,
Expand Down Expand Up @@ -1108,6 +1165,7 @@ export class CIHelper {
};
await this.maybeUpdateGGGNotes();
const mailArchiveGit = await MailArchiveGitHelper.get(
this.config,
this.notes,
mailArchiveGitDir,
this.github,
Expand Down
8 changes: 8 additions & 0 deletions lib/git-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ export class GitNotes {
return notes.replace(/^[^]*\n\n/, "");
}

public async initializeWithEmptyCommit(): Promise<void> {
const emptyTree = await git(["hash-object", "-t", "tree", "--stdin"], { stdin: "", workDir: this.workDir });
const emptyCommit = await git(["commit-tree", "-m", "Initial empty commit", emptyTree], {
workDir: this.workDir,
});
await git(["update-ref", this.notesRef, emptyCommit, ""], { workDir: this.workDir });
}

public async update(url: string): Promise<void> {
if (this.notesRef.match(/^refs\/notes\/(gitgitgadget|commit-to-mail|mail-to-commit)$/)) {
await git(["fetch", "--no-tags", url, `+${this.notesRef}:${this.notesRef}`], { workDir: this.workDir });
Expand Down
22 changes: 15 additions & 7 deletions lib/gitgitgadget-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IConfig, setConfig } from "./project-config.js";
import { IConfig } from "./project-config.js";

const defaultConfig: IConfig = {
repo: {
Expand Down Expand Up @@ -27,6 +27,8 @@ const defaultConfig: IConfig = {
mail: {
author: "GitGitGadget",
sender: "GitGitGadget",
smtpUser: "gitgitgadget@gmail.com",
smtpHost: "smtp.gmail.com",
},
app: {
appID: 12836,
Expand All @@ -42,12 +44,18 @@ const defaultConfig: IConfig = {
user: {
allowUserAsLogin: false,
},
syncUpstreamBranches: [
{
sourceRepo: "gitster/git",
targetRepo: "gitgitgadget/git",
sourceRefRegex: "^refs/heads/(maint-\\d|[a-z][a-z]/)",
},
{
sourceRepo: "j6t/git-gui",
targetRepo: "gitgitgadget/git",
targetRefNamespace: "git-gui/",
},
],
};

export default defaultConfig;

setConfig(defaultConfig);

export function getConfig(): IConfig {
return setConfig(defaultConfig);
}
18 changes: 15 additions & 3 deletions lib/gitgitgadget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IGitHubUser, IPullRequestInfo } from "./github-glue.js";
import { PatchSeries, SendFunction } from "./patch-series.js";
import { IPatchSeriesMetadata } from "./patch-series-metadata.js";
import { PatchSeriesOptions } from "./patch-series-options.js";
import { IConfig, getConfig } from "./project-config.js";
import { IConfig } from "./project-config.js";
import { ISMTPOptions, parseHeadersAndSendMail, parseMBox, sendMail } from "./send-mail.js";

export interface IGitGitGadgetOptions {
Expand Down Expand Up @@ -37,6 +37,7 @@ export class GitGitGadget {
}

public static async get(
config: IConfig,
gitGitGadgetDir: string,
workDir?: string,
publishTagsAndNotesToRemote?: string,
Expand Down Expand Up @@ -90,7 +91,15 @@ export class GitGitGadget {

const [options, allowedUsers] = await GitGitGadget.readOptions(notes);

return new GitGitGadget(notes, options, allowedUsers, smtpOptions, publishTagsAndNotesToRemote, notesPushToken);
return new GitGitGadget(
config,
notes,
options,
allowedUsers,
smtpOptions,
publishTagsAndNotesToRemote,
notesPushToken,
);
}

protected static async readOptions(notes: GitNotes): Promise<[IGitGitGadgetOptions, Set<string>]> {
Expand All @@ -103,7 +112,7 @@ export class GitGitGadget {
return [options, allowedUsers];
}

public readonly config: IConfig = getConfig();
public readonly config: IConfig;
public readonly workDir: string;
public readonly notes: GitNotes;
protected options: IGitGitGadgetOptions;
Expand All @@ -115,6 +124,7 @@ export class GitGitGadget {
private readonly publishToken: string | undefined;

protected constructor(
config: IConfig,
notes: GitNotes,
options: IGitGitGadgetOptions,
allowedUsers: Set<string>,
Expand All @@ -125,6 +135,7 @@ export class GitGitGadget {
if (!notes.workDir) {
throw new Error("Could not determine Git worktree");
}
this.config = config;
this.workDir = notes.workDir;
this.notes = notes;
this.options = options;
Expand Down Expand Up @@ -291,6 +302,7 @@ export class GitGitGadget {
options.rfc = pr.draft ?? false;

const series = await PatchSeries.getFromNotes(
this.config,
this.notes,
pr.pullRequestURL,
pr.title,
Expand Down
9 changes: 6 additions & 3 deletions lib/mail-archive-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IGitGitGadgetOptions } from "./gitgitgadget.js";
import { GitHubGlue } from "./github-glue.js";
import { IMailMetadata } from "./mail-metadata.js";
import { IPatchSeriesMetadata } from "./patch-series-metadata.js";
import { IConfig, getConfig } from "./project-config.js";
import { IConfig } from "./project-config.js";
import { getPullRequestKey } from "./pullRequestKey.js";
import { IParsedMBox, parseMBox, parseMBoxMessageIDAndReferences } from "./send-mail.js";
import { SousChef } from "./sous-chef.js";
Expand All @@ -19,13 +19,14 @@ export interface IGitMailingListMirrorState {

export class MailArchiveGitHelper {
public static async get(
config: IConfig,
gggNotes: GitNotes,
mailArchiveGitDir: string,
githubGlue: GitHubGlue,
branch: string,
): Promise<MailArchiveGitHelper> {
const state: IGitMailingListMirrorState = (await gggNotes.get<IGitMailingListMirrorState>(stateKey)) || {};
return new MailArchiveGitHelper(gggNotes, mailArchiveGitDir, githubGlue, state, branch);
return new MailArchiveGitHelper(config, gggNotes, mailArchiveGitDir, githubGlue, state, branch);
}

/**
Expand Down Expand Up @@ -56,19 +57,21 @@ export class MailArchiveGitHelper {
}

protected readonly branch: string;
protected readonly config: IConfig = getConfig();
protected readonly config: IConfig;
protected readonly state: IGitMailingListMirrorState;
protected readonly gggNotes: GitNotes;
protected readonly mailArchiveGitDir: string;
protected readonly githubGlue: GitHubGlue;

protected constructor(
config: IConfig,
gggNotes: GitNotes,
mailArchiveGitDir: string,
githubGlue: GitHubGlue,
state: IGitMailingListMirrorState,
branch: string,
) {
this.config = config;
this.branch = branch;
this.gggNotes = gggNotes;
this.mailArchiveGitDir = mailArchiveGitDir;
Expand Down
Loading
Loading