feat: [CODE-3500] add Git LFS support to the repository setting (#3672)

This commit is contained in:
Atefeh Mohseni Ejiyeh 2025-04-16 15:59:23 +00:00 committed by Harness
parent b8603e7f07
commit df94dca3a7
20 changed files with 217 additions and 26 deletions

View File

@ -62,7 +62,18 @@ func (c *Controller) processObjects(
return fmt.Errorf("failed to check settings for principal committer match: %w", err)
}
if sizeLimit == 0 && !principalCommitterMatch {
gitLFSEnabled, err := settings.RepoGet(
ctx,
c.settings,
repo.ID,
settings.KeyGitLFSEnabled,
settings.DefaultGitLFSEnabled,
)
if err != nil {
return fmt.Errorf("failed to check settings for Git LFS enabled: %w", err)
}
if sizeLimit == 0 && !principalCommitterMatch && !gitLFSEnabled {
return nil
}
@ -85,7 +96,9 @@ func (c *Controller) processObjects(
}
}
preReceiveObjsIn.FindLFSPointersParams = &git.FindLFSPointersParams{}
if gitLFSEnabled {
preReceiveObjsIn.FindLFSPointersParams = &git.FindLFSPointersParams{}
}
preReceiveObjsOut, err := c.git.ProcessPreReceiveObjects(
ctx,

View File

@ -18,8 +18,10 @@ import (
"context"
"fmt"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/auth/authn"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/app/token"
)
@ -28,6 +30,26 @@ func (c *Controller) Authenticate(
session *auth.Session,
repoRef string,
) (*AuthenticateResponse, error) {
repo, err := c.repoFinder.FindByRef(ctx, repoRef)
if err != nil {
return nil, fmt.Errorf("failed to find repository: %w", err)
}
gitLFSEnabled, err := settings.RepoGet(
ctx,
c.settings,
repo.ID,
settings.KeyGitLFSEnabled,
settings.DefaultGitLFSEnabled,
)
if err != nil {
return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err)
}
if !gitLFSEnabled {
return nil, usererror.ErrGitLFSDisabled
}
jwt, err := c.remoteAuth.GenerateToken(ctx, session.Principal.ID, session.Principal.Type, repoRef)
if err != nil {
return nil, fmt.Errorf("failed to generate auth token: %w", err)

View File

@ -24,6 +24,7 @@ import (
"github.com/harness/gitness/app/auth/authz"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/services/remoteauth"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/blob"
@ -43,6 +44,7 @@ type Controller struct {
blobStore blob.Store
remoteAuth remoteauth.Service
urlProvider url.Provider
settings *settings.Service
}
func NewController(
@ -53,6 +55,7 @@ func NewController(
blobStore blob.Store,
remoteAuth remoteauth.Service,
urlProvider url.Provider,
settings *settings.Service,
) *Controller {
return &Controller{
authorizer: authorizer,
@ -62,10 +65,11 @@ func NewController(
blobStore: blobStore,
remoteAuth: remoteAuth,
urlProvider: urlProvider,
settings: settings,
}
}
func (c *Controller) getRepoCheckAccess(
func (c *Controller) getRepoCheckAccessAndSetting(
ctx context.Context,
session *auth.Session,
repoRef string,
@ -89,6 +93,21 @@ func (c *Controller) getRepoCheckAccess(
return nil, fmt.Errorf("access check failed: %w", err)
}
gitLFSEnabled, err := settings.RepoGet(
ctx,
c.settings,
repo.ID,
settings.KeyGitLFSEnabled,
settings.DefaultGitLFSEnabled,
)
if err != nil {
return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err)
}
if !gitLFSEnabled {
return nil, usererror.ErrGitLFSDisabled
}
return repo, nil
}

View File

@ -41,7 +41,7 @@ func (c *Controller) Download(ctx context.Context,
repoRef string,
oid string,
) (*Content, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, enum.PermissionRepoView)
if err != nil {
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
}

View File

@ -37,7 +37,7 @@ func (c *Controller) LFSTransfer(ctx context.Context,
reqPermission = enum.PermissionRepoPush
}
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, reqPermission)
repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, reqPermission)
if err != nil {
return nil, err
}

View File

@ -39,7 +39,7 @@ func (c *Controller) Upload(ctx context.Context,
pointer Pointer,
file io.Reader,
) (*UploadOut, error) {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush)
repo, err := c.getRepoCheckAccessAndSetting(ctx, session, repoRef, enum.PermissionRepoPush)
if err != nil {
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
}

View File

@ -18,6 +18,7 @@ import (
"github.com/harness/gitness/app/auth/authz"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/services/remoteauth"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/blob"
@ -37,6 +38,16 @@ func ProvideController(
blobStore blob.Store,
remoteAuth remoteauth.Service,
urlProvider url.Provider,
settings *settings.Service,
) *Controller {
return NewController(authorizer, repoFinder, principalStore, lfsStore, blobStore, remoteAuth, urlProvider)
return NewController(
authorizer,
repoFinder,
principalStore,
lfsStore,
blobStore,
remoteAuth,
urlProvider,
settings,
)
}

View File

@ -22,9 +22,12 @@ import (
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git"
"github.com/harness/gitness/git/parser"
"github.com/harness/gitness/git/sha"
gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/types/enum"
)
@ -89,32 +92,52 @@ func (c *Controller) Raw(ctx context.Context,
return nil, fmt.Errorf("failed to read blob: %w", err)
}
// check if blob is an LFS pointer
content, err := io.ReadAll(io.LimitReader(blobReader.Content, parser.LfsPointerMaxSize))
gitLFSEnabled, err := settings.RepoGet(
ctx,
c.settings,
repo.ID,
settings.KeyGitLFSEnabled,
settings.DefaultGitLFSEnabled,
)
if err != nil {
return nil, fmt.Errorf("failed to read LFS file content: %w", err)
return nil, fmt.Errorf("failed to check settings for Git LFS enabled: %w", err)
}
lfsInfo, ok := parser.IsLFSPointer(ctx, content, blobReader.Size)
if !ok {
if !gitLFSEnabled {
return &RawContent{
Data: &multiReadCloser{
Reader: io.MultiReader(bytes.NewBuffer(content), blobReader.Content),
closeFunc: blobReader.Content.Close,
},
Data: blobReader.Content,
Size: blobReader.ContentSize,
SHA: blobReader.SHA,
}, nil
}
file, err := c.lfsCtrl.DownloadNoAuth(ctx, repo.ID, lfsInfo.OID)
// check if blob is an LFS pointer
headerContent, err := io.ReadAll(io.LimitReader(blobReader.Content, parser.LfsPointerMaxSize))
if err != nil {
return nil, fmt.Errorf("failed to download LFS file: %w", err)
return nil, fmt.Errorf("failed to read content: %w", err)
}
lfsInfo, ok := parser.IsLFSPointer(ctx, headerContent, blobReader.Size)
if ok {
lfsContent, err := c.lfsCtrl.DownloadNoAuth(ctx, repo.ID, lfsInfo.OID)
if err == nil {
return &RawContent{
Data: lfsContent,
Size: lfsInfo.Size,
SHA: blobReader.SHA,
}, nil
}
if !errors.Is(err, gitness_store.ErrResourceNotFound) {
return nil, fmt.Errorf("failed to download LFS file: %w", err)
}
}
return &RawContent{
Data: file,
Size: lfsInfo.Size,
Data: &multiReadCloser{
Reader: io.MultiReader(bytes.NewBuffer(headerContent), blobReader.Content),
closeFunc: blobReader.Content.Close,
},
Size: blobReader.ContentSize,
SHA: blobReader.SHA,
}, nil
}

View File

@ -23,17 +23,20 @@ import (
// GeneralSettings represent the general repository settings as exposed externally.
type GeneralSettings struct {
FileSizeLimit *int64 `json:"file_size_limit" yaml:"file_size_limit"`
GitLFSEnabled *bool `json:"git_lfs_enabled" yaml:"git_lfs_enabled"`
}
func GetDefaultGeneralSettings() *GeneralSettings {
return &GeneralSettings{
FileSizeLimit: ptr.Int64(settings.DefaultFileSizeLimit),
GitLFSEnabled: ptr.Bool(settings.DefaultGitLFSEnabled),
}
}
func GetGeneralSettingsMappings(s *GeneralSettings) []settings.SettingHandler {
return []settings.SettingHandler{
settings.Mapping(settings.KeyFileSizeLimit, s.FileSizeLimit),
settings.Mapping(settings.KeyGitLFSEnabled, s.GitLFSEnabled),
}
}
@ -46,5 +49,13 @@ func GetGeneralSettingsAsKeyValues(s *GeneralSettings) []settings.KeyValue {
Value: s.FileSizeLimit,
})
}
if s.GitLFSEnabled != nil {
kvs = append(kvs, settings.KeyValue{
Key: settings.KeyGitLFSEnabled,
Value: s.GitLFSEnabled,
})
}
return kvs
}

View File

@ -92,6 +92,9 @@ var (
// ErrEmptyRepoNeedsBranch is returned if no branch found on the githook post receieve for empty repositories.
ErrEmptyRepoNeedsBranch = New(http.StatusBadRequest,
"Pushing to an empty repository requires at least one branch with commits.")
// ErrGitLFSDisabled is returned if the Git LFS is disabled but LFS endpoint is requested.
ErrGitLFSDisabled = New(http.StatusBadRequest, "Git LFS is disabled")
)
// Error represents a json-encoded API error.

View File

@ -30,6 +30,7 @@ import (
"github.com/harness/gitness/app/services/keywordsearch"
"github.com/harness/gitness/app/services/publicaccess"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/app/sse"
"github.com/harness/gitness/app/store"
gitnessurl "github.com/harness/gitness/app/url"
@ -72,6 +73,7 @@ type Repository struct {
publicAccess publicaccess.Service
eventReporter *repoevents.Reporter
auditService audit.Service
settings *settings.Service
}
var _ job.Handler = (*Repository)(nil)
@ -295,6 +297,11 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
}
}
// revert this when import fetches LFS objects
if err := r.settings.RepoSet(ctx, repo.ID, settings.KeyGitLFSEnabled, false); err != nil {
log.Warn().Err(err).Msg("failed to disable Git LFS in repository settings")
}
log.Info().Msg("create git repository")
gitUID, err := r.createGitRepository(ctx, &systemPrincipal, repo.ID)

View File

@ -19,6 +19,7 @@ import (
"github.com/harness/gitness/app/services/keywordsearch"
"github.com/harness/gitness/app/services/publicaccess"
"github.com/harness/gitness/app/services/refcache"
"github.com/harness/gitness/app/services/settings"
"github.com/harness/gitness/app/sse"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
@ -53,6 +54,7 @@ func ProvideRepoImporter(
publicAccess publicaccess.Service,
eventReporter *repoevents.Reporter,
auditService audit.Service,
settings *settings.Service,
) (*Repository, error) {
importer := &Repository{
defaultBranch: config.Git.DefaultBranch,
@ -70,6 +72,7 @@ func ProvideRepoImporter(
publicAccess: publicAccess,
eventReporter: eventReporter,
auditService: auditService,
settings: settings,
}
err := executor.Register(jobType, importer)

View File

@ -26,4 +26,6 @@ var (
DefaultInstallID = string("")
KeyPrincipalCommitterMatch Key = "principal_committer_match"
DefaultPrincipalCommitterMatch = false
KeyGitLFSEnabled Key = "git_lfs_enabled"
DefaultGitLFSEnabled = true
)

View File

@ -260,7 +260,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
return nil, err
}
auditService := audit.ProvideAuditService()
repository, err := importer.ProvideRepoImporter(config, provider, gitInterface, transactor, repoStore, pipelineStore, triggerStore, repoFinder, encrypter, jobScheduler, executor, streamer, indexer, publicaccessService, eventsReporter, auditService)
repository, err := importer.ProvideRepoImporter(config, provider, gitInterface, transactor, repoStore, pipelineStore, triggerStore, repoFinder, encrypter, jobScheduler, executor, streamer, indexer, publicaccessService, eventsReporter, auditService, settingsService)
if err != nil {
return nil, err
}
@ -296,7 +296,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
return nil, err
}
remoteauthService := remoteauth.ProvideRemoteAuth(tokenStore, principalStore)
lfsController := lfs.ProvideController(authorizer, repoFinder, principalStore, lfsObjectStore, blobStore, remoteauthService, provider)
lfsController := lfs.ProvideController(authorizer, repoFinder, principalStore, lfsObjectStore, blobStore, remoteauthService, provider, settingsService)
repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, executionStore, ruleStore, checkStore, pullReqStore, settingsService, principalInfoCache, protectionManager, gitInterface, spaceFinder, repoFinder, repository, codeownersService, eventsReporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService, instrumentService, userGroupStore, searchService, rulesService, streamer, lfsController)
reposettingsController := reposettings.ProvideController(authorizer, repoFinder, settingsService, auditService)
stageStore := database.ProvideStageStore(db)

View File

@ -464,6 +464,9 @@ export interface StringsMap {
findOrCreateBranch: string
firstTimeTitle: string
general: string
'generalSetting.features': string
'generalSetting.gitLFSEnable': string
'generalSetting.gitLFSEnableDesc': string
generate: string
generateCloneCred: string
generateCloneText: string

View File

@ -397,6 +397,10 @@ prReview:
targetBranchChange: '{user} changed the target branch from {old} to {new}'
webhookListingContent: 'create,delete,deployment ...'
general: 'General'
generalSetting:
features: Features
gitLFSEnable: Git Large File Storage (LFS)
gitLFSEnableDesc: Manage large files using Git LFS.
webhooks: 'Webhooks'
open: Open
merged: Merged

View File

@ -32,7 +32,7 @@ import cx from 'classnames'
import { Color, FontVariation, Intent } from '@harnessio/design-system'
import { Icon } from '@harnessio/icons'
import { noop } from 'lodash-es'
import { useMutate } from 'restful-react'
import { useGet, useMutate } from 'restful-react'
import { Render } from 'react-jsx-match'
import { ACCESS_MODES, getErrorMessage, permissionProps, voidFn } from 'utils/Utils'
import { useStrings } from 'framework/strings'
@ -64,7 +64,7 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
const [defaultBranch, setDefaultBranch] = useState(ACCESS_MODES.VIEW)
const { openModal: openDefaultBranchModal } = useDefaultBranchModal({ currentGitRef, setDefaultBranch, refetch })
const { showError, showSuccess } = useToaster()
const { standalone, hooks } = useAppContext()
const { standalone, hooks, routingId } = useAppContext()
const space = useGetSpaceParam()
const { allowPublicResourceCreation } = usePublicResourceConfig()
const { getString } = useStrings()
@ -83,6 +83,17 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
path: `/api/v1/repos/${repoMetadata?.path}/+/public-access`
})
const { data: generalSettingsData, refetch: refetchSettings } = useGet({
path: `/api/v1/repos/${repoMetadata?.path}/+/settings/general`,
queryParams: { routingId: routingId }
})
const { mutate: updateGeneralSettings } = useMutate({
verb: 'PATCH',
path: `/api/v1/repos/${repoMetadata?.path}/+/settings/general`,
queryParams: { routingId: routingId }
})
const permEditResult = hooks?.usePermissionTranslate?.(
{
resource: {
@ -178,12 +189,14 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
return (
<Formik
enableReinitialize
formName="repoGeneralSettings"
initialValues={{
name: repoMetadata?.identifier,
desc: repoMetadata?.description,
defaultBranch: repoMetadata?.default_branch,
isPublic: currRepoVisibility
isPublic: currRepoVisibility,
gitLFSEnabled: generalSettingsData?.git_lfs_enabled ?? true
}}
onSubmit={voidFn(mutate)}>
{formik => {
@ -412,6 +425,56 @@ const GeneralSettingsContent = (props: GeneralSettingsProps) => {
</Layout.Horizontal>
</Container>
</Render>
<Container padding="medium" margin={{ bottom: 'medium' }} className={css.generalContainer}>
<Layout.Horizontal padding={{ bottom: 'medium' }}>
<Container className={css.label}>
<Text color={Color.GREY_600} className={css.textSize} margin={{ top: 'medium' }}>
{getString('generalSetting.features')}
</Text>
</Container>
<Layout.Vertical spacing="small" padding={{ button: 'small', top: 'small' }}>
<Container className={css.content}>
<Layout.Horizontal flex={{ alignItems: 'center' }} spacing={'small'}>
<FormInput.Toggle
{...permissionProps(permEditResult, standalone)}
key={'gitLFSEnabled'}
style={{ margin: '0px' }}
label=""
name="gitLFSEnabled"
/>
<Text color={Color.GREY_800} className={css.featureText}>
{getString('generalSetting.gitLFSEnable')}
</Text>
<Text color={Color.GREY_500} className={css.featureText}>
{getString('generalSetting.gitLFSEnableDesc')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal className={css.buttonContainer}>
{generalSettingsData?.git_lfs_enabled !== formik.values.gitLFSEnabled ? (
<Button
margin={{ top: 'medium' }}
type="submit"
text={getString('save')}
variation={ButtonVariation.PRIMARY}
size={ButtonSize.SMALL}
onClick={() => {
updateGeneralSettings({ git_lfs_enabled: formik.values.gitLFSEnabled })
.then(() => {
showSuccess(getString('repoUpdate'))
refetchSettings()
})
.catch(err => {
showError(getErrorMessage(err))
})
}}
{...permissionProps(permEditResult, standalone)}
/>
) : null}
</Layout.Horizontal>
</Container>
</Layout.Vertical>
</Layout.Horizontal>
</Container>
<Container padding="medium" margin={{ bottom: 'medium' }} className={css.generalContainer}>
<Layout.Horizontal padding={{ bottom: 'medium' }}>
<Container className={css.label}>

View File

@ -117,6 +117,13 @@
text-transform: capitalize;
}
.featureText {
font-size: 12px !important;
font-style: normal !important;
font-weight: 300 !important;
white-space: nowrap !important;
}
.dividerContainer {
opacity: 0.2;
height: 1px;

View File

@ -26,6 +26,7 @@ export declare const descText: string
export declare const dialogContainer: string
export declare const dividerContainer: string
export declare const editContainer: string
export declare const featureText: string
export declare const generalContainer: string
export declare const headerContainer: string
export declare const iconContainer: string

View File

@ -48,7 +48,6 @@ export default function RepositorySettings() {
gitRef: normalizeGitRef(gitRef) as string,
resourcePath
})
useDisableCodeMainLinks(!!isRepositoryEmpty)
const tabListArray = [
{