Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions components/dashboard/src/data/featureflag-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const featureFlags = {
enable_experimental_jbtb: false,
enabled_configuration_prebuild_full_clone: false,
enterprise_onboarding_enabled: false,
commit_annotation_setting_enabled: false,
};

type FeatureFlags = typeof featureFlags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type UpdateOrganizationSettingsArgs = Partial<
| "roleRestrictions"
| "maxParallelRunningWorkspaces"
| "onboardingSettings"
| "annotateGitCommits"
>
>;

Expand All @@ -47,6 +48,7 @@ export const useUpdateOrgSettingsMutation = () => {
roleRestrictions,
maxParallelRunningWorkspaces,
onboardingSettings,
annotateGitCommits,
}) => {
const settings = await organizationClient.updateOrganizationSettings({
organizationId: teamId,
Expand All @@ -63,6 +65,7 @@ export const useUpdateOrgSettingsMutation = () => {
updateRoleRestrictions: !!roleRestrictions,
maxParallelRunningWorkspaces,
onboardingSettings,
annotateGitCommits,
});
return settings.settings!;
},
Expand Down
64 changes: 53 additions & 11 deletions components/dashboard/src/teams/TeamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,39 @@
* See License.AGPL.txt in the project root for license information.
*/

import { PlainMessage } from "@bufbuild/protobuf";
import { EnvVar } from "@gitpod/gitpod-protocol";
import { ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
import { Button } from "@podkit/buttons/Button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@podkit/select/Select";
import { SwitchInputField } from "@podkit/switch/Switch";
import { Heading2, Heading3, Subheading } from "@podkit/typography/Headings";
import React, { Children, ReactNode, useCallback, useMemo, useState } from "react";
import Alert from "../components/Alert";
import ConfirmationModal from "../components/ConfirmationModal";
import { InputWithCopy } from "../components/InputWithCopy";
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
import { InputField } from "../components/forms/InputField";
import { TextInputField } from "../components/forms/TextInputField";
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
import { useToast } from "../components/toasts/Toasts";
import { useFeatureFlag } from "../data/featureflag-query";
import { useInstallationDefaultWorkspaceImageQuery } from "../data/installation/default-workspace-image-query";
import { useIsOwner } from "../data/organizations/members-query";
import { useListOrganizationEnvironmentVariables } from "../data/organizations/org-envvar-queries";
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
import { useCurrentOrg, useOrganizationsInvalidator } from "../data/organizations/orgs-query";
import { useUpdateOrgMutation } from "../data/organizations/update-org-mutation";
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
import { useDocumentTitle } from "../hooks/use-document-title";
import { useOnBlurError } from "../hooks/use-onblur-error";
import { ReactComponent as Stack } from "../icons/Stack.svg";
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
import { organizationClient } from "../service/public-api";
import { gitpodHostUrl } from "../service/service";
import { useCurrentUser } from "../user-context";
import { OrgSettingsPage } from "./OrgSettingsPage";
import { ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { Button } from "@podkit/buttons/Button";
import { useInstallationDefaultWorkspaceImageQuery } from "../data/installation/default-workspace-image-query";
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@podkit/select/Select";
import { useDocumentTitle } from "../hooks/use-document-title";
import { PlainMessage } from "@bufbuild/protobuf";
import { useToast } from "../components/toasts/Toasts";
import { NamedOrganizationEnvvarItem } from "./variables/NamedOrganizationEnvvarItem";
import { useListOrganizationEnvironmentVariables } from "../data/organizations/org-envvar-queries";
import { EnvVar } from "@gitpod/gitpod-protocol";

export default function TeamSettingsPage() {
useDocumentTitle("Organization Settings - General");
Expand All @@ -53,6 +55,7 @@ export default function TeamSettingsPage() {
const gitpodImageAuthEnvVar = orgEnvVars.data?.find((v) => v.name === EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME);

const updateOrg = useUpdateOrgMutation();
const isCommitAnnotationEnabled = useFeatureFlag("commit_annotation_setting_enabled");

const close = () => setModal(false);

Expand Down Expand Up @@ -128,6 +131,17 @@ export default function TeamSettingsPage() {
[updateTeamSettings, org?.id, isOwner, settings, toast],
);

const handleUpdateAnnotatedCommits = useCallback(
async (value: boolean) => {
try {
await handleUpdateTeamSettings({ annotateGitCommits: value });
} catch (error) {
console.error(error);
}
},
[handleUpdateTeamSettings],
);

return (
<>
<OrgSettingsPage>
Expand Down Expand Up @@ -213,6 +227,34 @@ export default function TeamSettingsPage() {
/>
</ConfigurationSettingsField>

{isCommitAnnotationEnabled && (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, how we indicate what kind of settings should be on General and Policies? (I was trying to find this setting on Policies tab before look through the code)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mustard-mh I generally go with my gut here. This time around, I didn't consider this feature a policy, since it's not restricting anything per se, like our other policies we have on that page.

It could definitely be argued that "it's asserting that all of an organization's commits have our Tool: trailer", though, so maybe? What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion there, just curious 😋 Any place is good for me (Once the docs to the user is clear, and for sure you will do it)

<ConfigurationSettingsField>
<Heading3>Insights</Heading3>
<Subheading className="mb-4">
Configure insights into usage of Gitpod in your organization.
</Subheading>

<InputField
label="Annotate git commits"
hint={
<>
Add a <code>Tool:</code> field to all git commit messages created from
workspaces in your organization to associate them with this Gitpod instance.
</>
}
id="annotate-git-commits"
>
<SwitchInputField
id="annotate-git-commits"
checked={settings?.annotateGitCommits || false}
disabled={!isOwner || isLoading}
onCheckedChange={handleUpdateAnnotatedCommits}
label=""
/>
</InputField>
</ConfigurationSettingsField>
)}

{showImageEditModal && (
<OrgDefaultWorkspaceImageModal
settings={settings}
Expand Down
71 changes: 71 additions & 0 deletions components/gitpod-cli/cmd/git-commit-message-helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"context"
"fmt"
"os"
"os/exec"
"time"

"github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var gitCommitMessageHelperOpts struct {
CommitMessageFile string
}

func addGitpodTrailer(commitMsgFile string, hostName string) error {
trailerCmd := exec.Command("git", "interpret-trailers",
"--if-exists", "addIfDifferent",
"--trailer", fmt.Sprintf("Tool: gitpod/%s", hostName),
commitMsgFile)

output, err := trailerCmd.Output()
if err != nil {
return fmt.Errorf("error adding trailer: %w", err)
}

err = os.WriteFile(commitMsgFile, output, 0644)
if err != nil {
return fmt.Errorf("error writing commit message file: %w", err)
}

return nil
}

var gitCommitMessageHelper = &cobra.Command{
Use: "git-commit-message-helper",
Short: "Gitpod's Git commit message helper",
Long: "Automatically adds Tool information to Git commit messages",
Args: cobra.ExactArgs(0),
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Second)
defer cancel()

wsInfo, err := gitpod.GetWSInfo(ctx)
if err != nil {
log.WithError(err).Fatal("error getting workspace info")
return nil // don't block commit
}

if err := addGitpodTrailer(gitCommitMessageHelperOpts.CommitMessageFile, wsInfo.GitpodApi.Host); err != nil {
log.WithError(err).Fatal("failed to add gitpod trailer")
return nil // don't block commit
}

return nil
},
}

func init() {
rootCmd.AddCommand(gitCommitMessageHelper)
gitCommitMessageHelper.Flags().StringVarP(&gitCommitMessageHelperOpts.CommitMessageFile, "file", "f", "", "Path to the commit message file")
_ = gitCommitMessageHelper.MarkFlagRequired("file")
}
74 changes: 74 additions & 0 deletions components/gitpod-cli/cmd/git-commit-message-helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2025 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package cmd

import (
"os"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestAddGitpodTrailer(t *testing.T) {
tests := []struct {
Name string
CommitMsg string
HostName string
Expected string
ExpectError bool
}{
{
Name: "adds trailer to simple message",
CommitMsg: "Initial commit",
HostName: "gitpod.io",
Expected: "Initial commit\n\nTool: gitpod/gitpod.io\n",
ExpectError: false,
},
{
Name: "doesn't duplicate existing trailer",
CommitMsg: "Initial commit\n\nTool: gitpod/gitpod.io\n",
HostName: "gitpod.io",
Expected: "Initial commit\n\nTool: gitpod/gitpod.io\n",
ExpectError: false,
},
{
Name: "preserves other trailers",
CommitMsg: "Initial commit\n\nSigned-off-by: Kyle <[email protected]>\n",
HostName: "gitpod.io",
Expected: "Initial commit\n\nSigned-off-by: Kyle <[email protected]>\nTool: gitpod/gitpod.io\n",
ExpectError: false,
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "commit-msg-*")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())

if err := os.WriteFile(tmpfile.Name(), []byte(tt.CommitMsg), 0644); err != nil {
t.Fatal(err)
}

err = addGitpodTrailer(tmpfile.Name(), tt.HostName)
if (err != nil) != tt.ExpectError {
t.Errorf("addGitpodTrailer() error = %v, wantErr %v", err, tt.ExpectError)
return
}

got, err := os.ReadFile(tmpfile.Name())
if err != nil {
t.Fatal(err)
}

equal := cmp.Equal(string(got), tt.Expected)
if !equal {
t.Fatalf(`Detected git command info was incorrect, got: %v, expected: %v.`, string(got), tt.Expected)
}
})
}
}
3 changes: 3 additions & 0 deletions components/gitpod-db/src/typeorm/entity/db-team-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export class DBOrgSettings implements OrganizationSettings {
@Column("json", { nullable: true })
onboardingSettings?: OnboardingSettings | undefined;

@Column({ type: "boolean", default: false })
annotateGitCommits?: boolean | undefined;

@Column()
deleted: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

import { MigrationInterface, QueryRunner } from "typeorm";
import { columnExists } from "./helper/helper";

const table = "d_b_org_settings";
const newColumn = "annotateGitCommits";

export class AddOrgSettingsCommitAnnotation1736951418625 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await columnExists(queryRunner, table, newColumn))) {
await queryRunner.query(`ALTER TABLE ${table} ADD COLUMN ${newColumn} BOOLEAN DEFAULT FALSE`);
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
if (await columnExists(queryRunner, table, newColumn)) {
await queryRunner.query(`ALTER TABLE ${table} DROP COLUMN ${newColumn}`);
}
}
}
1 change: 1 addition & 0 deletions components/gitpod-db/src/typeorm/team-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ export class TeamDBImpl extends TransactionalDBImpl<TeamDB> implements TeamDB {
"roleRestrictions",
"maxParallelRunningWorkspaces",
"onboardingSettings",
"annotateGitCommits",
],
});
}
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/teams-projects-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ export interface OrganizationSettings {

// onboarding settings for the organization
onboardingSettings?: OnboardingSettings;

// whether to add a special annotation to commits that are created through Gitpod
annotateGitCommits?: boolean;
}

export type TimeoutSettings = {
Expand Down
4 changes: 4 additions & 0 deletions components/public-api/gitpod/v1/organization.proto
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ message OrganizationSettings {
// max_parallel_running_workspaces is the maximum number of workspaces that a single user can run in parallel. 0 resets to the default, which depends on the org plan
int32 max_parallel_running_workspaces = 9;
OnboardingSettings onboarding_settings = 10;
bool annotate_git_commits = 11;
}

service OrganizationService {
Expand Down Expand Up @@ -193,6 +194,9 @@ message UpdateOrganizationSettingsRequest {

// onboarding_settings are the settings for the organization's onboarding
optional OnboardingSettings onboarding_settings = 16;

// annotate_git_commits specifies whether to annotate git commits created in Gitpod workspaces with the gitpod host
optional bool annotate_git_commits = 17;
}

message UpdateOrganizationSettingsResponse {
Expand Down
Loading
Loading