loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[PERFORMANCE] git check-attr on bare repo if supported' (#2763) from oliverpool/forgejo:check_attr_bare into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2763
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+429 -365
+5 -3
modules/git/git.go
··· 33 33 // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx 34 34 DefaultContext context.Context 35 35 36 - SupportProcReceive bool // >= 2.29 37 - SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ 38 - InvertedGitFlushEnv bool // 2.43.1 36 + SupportProcReceive bool // >= 2.29 37 + SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ 38 + InvertedGitFlushEnv bool // 2.43.1 39 + SupportCheckAttrOnBare bool // >= 2.40 39 40 40 41 gitVersion *version.Version 41 42 ) ··· 187 188 } 188 189 SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil 189 190 SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit 191 + SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil 190 192 if SupportHashSha256 { 191 193 SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) 192 194 } else {
+182 -207
modules/git/repo_attribute.go
··· 7 7 "bytes" 8 8 "context" 9 9 "fmt" 10 - "io" 11 10 "os" 11 + "strings" 12 12 13 13 "code.gitea.io/gitea/modules/log" 14 14 "code.gitea.io/gitea/modules/optional" 15 15 ) 16 16 17 - // CheckAttributeOpts represents the possible options to CheckAttribute 18 - type CheckAttributeOpts struct { 19 - CachedOnly bool 20 - AllAttributes bool 21 - Attributes []string 22 - Filenames []string 23 - IndexFile string 24 - WorkTree string 25 - } 17 + var LinguistAttributes = []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"} 26 18 27 - // CheckAttribute return the Blame object of file 28 - func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { 29 - env := []string{} 19 + // GitAttribute exposes an attribute from the .gitattribute file 20 + type GitAttribute string //nolint:revive 30 21 31 - if len(opts.IndexFile) > 0 { 32 - env = append(env, "GIT_INDEX_FILE="+opts.IndexFile) 33 - } 34 - if len(opts.WorkTree) > 0 { 35 - env = append(env, "GIT_WORK_TREE="+opts.WorkTree) 36 - } 22 + // IsSpecified returns true if the gitattribute is set and not empty 23 + func (ca GitAttribute) IsSpecified() bool { 24 + return ca != "" && ca != "unspecified" 25 + } 37 26 38 - if len(env) > 0 { 39 - env = append(os.Environ(), env...) 27 + // String returns the value of the attribute or "" if unspecified 28 + func (ca GitAttribute) String() string { 29 + if !ca.IsSpecified() { 30 + return "" 40 31 } 41 - 42 - stdOut := new(bytes.Buffer) 43 - stdErr := new(bytes.Buffer) 44 - 45 - cmd := NewCommand(repo.Ctx, "check-attr", "-z") 32 + return string(ca) 33 + } 46 34 47 - if opts.AllAttributes { 48 - cmd.AddArguments("-a") 49 - } else { 50 - for _, attribute := range opts.Attributes { 51 - if attribute != "" { 52 - cmd.AddDynamicArguments(attribute) 53 - } 54 - } 35 + // Prefix returns the value of the attribute before any question mark '?' 36 + // 37 + // sometimes used within gitlab-language: https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type 38 + func (ca GitAttribute) Prefix() string { 39 + s := ca.String() 40 + if i := strings.IndexByte(s, '?'); i >= 0 { 41 + return s[:i] 55 42 } 43 + return s 44 + } 56 45 57 - if opts.CachedOnly { 58 - cmd.AddArguments("--cached") 46 + // Bool returns true if "set"/"true", false if "unset"/"false", none otherwise 47 + func (ca GitAttribute) Bool() optional.Option[bool] { 48 + switch ca { 49 + case "set", "true": 50 + return optional.Some(true) 51 + case "unset", "false": 52 + return optional.Some(false) 59 53 } 54 + return optional.None[bool]() 55 + } 60 56 61 - cmd.AddDashesAndList(opts.Filenames...) 62 - 63 - if err := cmd.Run(&RunOpts{ 64 - Env: env, 65 - Dir: repo.Path, 66 - Stdout: stdOut, 67 - Stderr: stdErr, 68 - }); err != nil { 69 - return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) 57 + // GitAttributeFirst returns the first specified attribute 58 + // 59 + // If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare). 60 + func (repo *Repository) GitAttributeFirst(treeish, filename string, attributes ...string) (GitAttribute, error) { 61 + values, err := repo.GitAttributes(treeish, filename, attributes...) 62 + if err != nil { 63 + return "", err 70 64 } 71 - 72 - // FIXME: This is incorrect on versions < 1.8.5 73 - fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) 65 + for _, a := range attributes { 66 + if values[a].IsSpecified() { 67 + return values[a], nil 68 + } 69 + } 70 + return "", nil 71 + } 74 72 75 - if len(fields)%3 != 1 { 76 - return nil, fmt.Errorf("wrong number of fields in return from check-attr") 73 + func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string) (*Command, *RunOpts, context.CancelFunc, error) { 74 + if len(attributes) == 0 { 75 + return nil, nil, nil, fmt.Errorf("no provided attributes to check-attr") 77 76 } 78 77 79 - name2attribute2info := make(map[string]map[string]string) 78 + env := os.Environ() 79 + var deleteTemporaryFile context.CancelFunc 80 80 81 - for i := 0; i < (len(fields) / 3); i++ { 82 - filename := string(fields[3*i]) 83 - attribute := string(fields[3*i+1]) 84 - info := string(fields[3*i+2]) 85 - attribute2info := name2attribute2info[filename] 86 - if attribute2info == nil { 87 - attribute2info = make(map[string]string) 81 + // git < 2.40 cannot run check-attr on bare repo, but needs INDEX + WORK_TREE 82 + hasIndex := treeish == "" 83 + if !hasIndex && !SupportCheckAttrOnBare { 84 + indexFilename, worktree, cancel, err := repo.ReadTreeToTemporaryIndex(treeish) 85 + if err != nil { 86 + return nil, nil, nil, err 88 87 } 89 - attribute2info[attribute] = info 90 - name2attribute2info[filename] = attribute2info 91 - } 88 + deleteTemporaryFile = cancel 92 89 93 - return name2attribute2info, nil 94 - } 90 + env = append(env, "GIT_INDEX_FILE="+indexFilename, "GIT_WORK_TREE="+worktree) 95 91 96 - // CheckAttributeReader provides a reader for check-attribute content that can be long running 97 - type CheckAttributeReader struct { 98 - // params 99 - Attributes []string 100 - Repo *Repository 101 - IndexFile string 102 - WorkTree string 92 + hasIndex = true 103 93 104 - stdinReader io.ReadCloser 105 - stdinWriter *os.File 106 - stdOut attributeWriter 107 - cmd *Command 108 - env []string 109 - ctx context.Context 110 - cancel context.CancelFunc 111 - } 112 - 113 - // Init initializes the CheckAttributeReader 114 - func (c *CheckAttributeReader) Init(ctx context.Context) error { 115 - if len(c.Attributes) == 0 { 116 - lw := new(nulSeparatedAttributeWriter) 117 - lw.attributes = make(chan attributeTriple) 118 - lw.closed = make(chan struct{}) 119 - 120 - c.stdOut = lw 121 - c.stdOut.Close() 122 - return fmt.Errorf("no provided Attributes to check") 94 + // clear treeish to read from provided index/work_tree 95 + treeish = "" 96 + } 97 + ctx, cancel := context.WithCancel(repo.Ctx) 98 + if deleteTemporaryFile != nil { 99 + ctxCancel := cancel 100 + cancel = func() { 101 + ctxCancel() 102 + deleteTemporaryFile() 103 + } 123 104 } 124 105 125 - c.ctx, c.cancel = context.WithCancel(ctx) 126 - c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z") 106 + cmd := NewCommand(ctx, "check-attr", "-z") 127 107 128 - if len(c.IndexFile) > 0 { 129 - c.cmd.AddArguments("--cached") 130 - c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile) 108 + if hasIndex { 109 + cmd.AddArguments("--cached") 131 110 } 132 111 133 - if len(c.WorkTree) > 0 { 134 - c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) 112 + if len(treeish) > 0 { 113 + cmd.AddArguments("--source") 114 + cmd.AddDynamicArguments(treeish) 135 115 } 116 + cmd.AddDynamicArguments(attributes...) 136 117 137 118 // Version 2.43.1 has a bug where the behavior of `GIT_FLUSH` is flipped. 138 119 // Ref: https://lore.kernel.org/git/CABn0oJvg3M_kBW-u=j3QhKnO=6QOzk-YFTgonYw_UvFS1NTX4g@mail.gmail.com 139 120 if InvertedGitFlushEnv { 140 - c.env = append(c.env, "GIT_FLUSH=0") 121 + env = append(env, "GIT_FLUSH=0") 141 122 } else { 142 - c.env = append(c.env, "GIT_FLUSH=1") 123 + env = append(env, "GIT_FLUSH=1") 143 124 } 144 125 145 - c.cmd.AddDynamicArguments(c.Attributes...) 146 - 147 - var err error 126 + return cmd, &RunOpts{ 127 + Env: env, 128 + Dir: repo.Path, 129 + }, cancel, nil 130 + } 148 131 149 - c.stdinReader, c.stdinWriter, err = os.Pipe() 132 + // GitAttributes returns gitattribute. 133 + // 134 + // If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare). 135 + func (repo *Repository) GitAttributes(treeish, filename string, attributes ...string) (map[string]GitAttribute, error) { 136 + cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...) 150 137 if err != nil { 151 - c.cancel() 152 - return err 138 + return nil, err 153 139 } 140 + defer cancel() 154 141 155 - lw := new(nulSeparatedAttributeWriter) 156 - lw.attributes = make(chan attributeTriple, 5) 157 - lw.closed = make(chan struct{}) 158 - c.stdOut = lw 159 - return nil 160 - } 142 + stdOut := new(bytes.Buffer) 143 + runOpts.Stdout = stdOut 161 144 162 - // Run run cmd 163 - func (c *CheckAttributeReader) Run() error { 164 - defer func() { 165 - _ = c.stdinReader.Close() 166 - _ = c.stdOut.Close() 167 - }() 168 145 stdErr := new(bytes.Buffer) 169 - err := c.cmd.Run(&RunOpts{ 170 - Env: c.env, 171 - Dir: c.Repo.Path, 172 - Stdin: c.stdinReader, 173 - Stdout: c.stdOut, 174 - Stderr: stdErr, 175 - }) 176 - if err != nil && // If there is an error we need to return but: 177 - c.ctx.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded) 178 - err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed 179 - return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) 180 - } 181 - return nil 182 - } 146 + runOpts.Stderr = stdErr 183 147 184 - // CheckPath check attr for given path 185 - func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { 186 - defer func() { 187 - if err != nil && err != c.ctx.Err() { 188 - log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err) 189 - } 190 - }() 148 + cmd.AddDashesAndList(filename) 191 149 192 - select { 193 - case <-c.ctx.Done(): 194 - return nil, c.ctx.Err() 195 - default: 150 + if err := cmd.Run(runOpts); err != nil { 151 + return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) 196 152 } 197 153 198 - if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { 199 - defer c.Close() 200 - return nil, err 154 + // FIXME: This is incorrect on versions < 1.8.5 155 + fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) 156 + 157 + if len(fields)%3 != 1 { 158 + return nil, fmt.Errorf("wrong number of fields in return from check-attr") 201 159 } 202 160 203 - rs = make(map[string]string) 204 - for range c.Attributes { 205 - select { 206 - case attr, ok := <-c.stdOut.ReadAttribute(): 207 - if !ok { 208 - return nil, c.ctx.Err() 209 - } 210 - rs[attr.Attribute] = attr.Value 211 - case <-c.ctx.Done(): 212 - return nil, c.ctx.Err() 213 - } 161 + values := make(map[string]GitAttribute, len(attributes)) 162 + for ; len(fields) >= 3; fields = fields[3:] { 163 + // filename := string(fields[0]) 164 + attribute := string(fields[1]) 165 + value := string(fields[2]) 166 + values[attribute] = GitAttribute(value) 214 167 } 215 - return rs, nil 216 - } 217 - 218 - // Close close pip after use 219 - func (c *CheckAttributeReader) Close() error { 220 - c.cancel() 221 - err := c.stdinWriter.Close() 222 - return err 223 - } 224 - 225 - type attributeWriter interface { 226 - io.WriteCloser 227 - ReadAttribute() <-chan attributeTriple 168 + return values, nil 228 169 } 229 170 230 171 type attributeTriple struct { ··· 275 216 return len(p), nil 276 217 } 277 218 278 - func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { 279 - return wr.attributes 280 - } 281 - 282 219 func (wr *nulSeparatedAttributeWriter) Close() error { 283 220 select { 284 221 case <-wr.closed: ··· 290 227 return nil 291 228 } 292 229 293 - // Create a check attribute reader for the current repository and provided commit ID 294 - func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { 295 - indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) 230 + // GitAttributeChecker creates an AttributeChecker for the given repository and provided commit ID. 231 + // 232 + // If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare). 233 + func (repo *Repository) GitAttributeChecker(treeish string, attributes ...string) (AttributeChecker, error) { 234 + cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...) 296 235 if err != nil { 297 - return nil, func() {} 236 + return AttributeChecker{}, err 298 237 } 299 238 300 - checker := &CheckAttributeReader{ 301 - Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}, 302 - Repo: repo, 303 - IndexFile: indexFilename, 304 - WorkTree: worktree, 239 + ac := AttributeChecker{ 240 + attributeNumber: len(attributes), 241 + ctx: cmd.parentContext, 242 + cancel: cancel, // will be cancelled on Close 305 243 } 306 - ctx, cancel := context.WithCancel(repo.Ctx) 307 - if err := checker.Init(ctx); err != nil { 308 - log.Error("Unable to open checker for %s. Error: %v", commitID, err) 309 - } else { 310 - go func() { 311 - err := checker.Run() 312 - if err != nil && err != ctx.Err() { 313 - log.Error("Unable to open checker for %s. Error: %v", commitID, err) 314 - } 315 - cancel() 316 - }() 244 + 245 + stdinReader, stdinWriter, err := os.Pipe() 246 + if err != nil { 247 + ac.cancel() 248 + return AttributeChecker{}, err 317 249 } 318 - deferable := func() { 319 - _ = checker.Close() 320 - cancel() 321 - deleteTemporaryFile() 322 - } 250 + ac.stdinWriter = stdinWriter // will be closed on Close 251 + 252 + lw := new(nulSeparatedAttributeWriter) 253 + lw.attributes = make(chan attributeTriple, len(attributes)) 254 + lw.closed = make(chan struct{}) 255 + ac.attributesCh = lw.attributes 256 + 257 + cmd.AddArguments("--stdin") 258 + go func() { 259 + defer stdinReader.Close() 260 + defer lw.Close() 261 + 262 + stdErr := new(bytes.Buffer) 263 + runOpts.Stdin = stdinReader 264 + runOpts.Stdout = lw 265 + runOpts.Stderr = stdErr 266 + err := cmd.Run(runOpts) 267 + 268 + if err != nil && // If there is an error we need to return but: 269 + cmd.parentContext.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded) 270 + err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed 271 + log.Error("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) 272 + } 273 + }() 274 + 275 + return ac, nil 276 + } 323 277 324 - return checker, deferable 278 + type AttributeChecker struct { 279 + ctx context.Context 280 + cancel context.CancelFunc 281 + stdinWriter *os.File 282 + attributeNumber int 283 + attributesCh <-chan attributeTriple 325 284 } 326 285 327 - // true if "set"/"true", false if "unset"/"false", none otherwise 328 - func attributeToBool(attr map[string]string, name string) optional.Option[bool] { 329 - if value, has := attr[name]; has && value != "unspecified" { 330 - switch value { 331 - case "set", "true": 332 - return optional.Some(true) 333 - case "unset", "false": 334 - return optional.Some(false) 286 + func (ac AttributeChecker) CheckPath(path string) (map[string]GitAttribute, error) { 287 + if err := ac.ctx.Err(); err != nil { 288 + return nil, err 289 + } 290 + 291 + if _, err := ac.stdinWriter.Write([]byte(path + "\x00")); err != nil { 292 + return nil, err 293 + } 294 + 295 + rs := make(map[string]GitAttribute) 296 + for i := 0; i < ac.attributeNumber; i++ { 297 + select { 298 + case attr, ok := <-ac.attributesCh: 299 + if !ok { 300 + return nil, ac.ctx.Err() 301 + } 302 + rs[attr.Attribute] = GitAttribute(attr.Value) 303 + case <-ac.ctx.Done(): 304 + return nil, ac.ctx.Err() 335 305 } 336 306 } 337 - return optional.None[bool]() 307 + return rs, nil 308 + } 309 + 310 + func (ac AttributeChecker) Close() error { 311 + ac.cancel() 312 + return ac.stdinWriter.Close() 338 313 }
+121 -8
modules/git/repo_attribute_test.go
··· 4 4 package git 5 5 6 6 import ( 7 + "path/filepath" 7 8 "testing" 8 9 "time" 9 10 11 + "code.gitea.io/gitea/modules/test" 12 + 10 13 "github.com/stretchr/testify/assert" 14 + "github.com/stretchr/testify/require" 11 15 ) 12 16 13 17 func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) { ··· 22 26 assert.Len(t, testStr, n) 23 27 assert.NoError(t, err) 24 28 select { 25 - case attr := <-wr.ReadAttribute(): 29 + case attr := <-wr.attributes: 26 30 assert.Equal(t, ".gitignore\"\n", attr.Filename) 27 31 assert.Equal(t, "linguist-vendored", attr.Attribute) 28 32 assert.Equal(t, "unspecified", attr.Value) ··· 36 40 assert.NoError(t, err) 37 41 38 42 select { 39 - case attr := <-wr.ReadAttribute(): 43 + case attr := <-wr.attributes: 40 44 assert.Equal(t, ".gitignore\"\n", attr.Filename) 41 45 assert.Equal(t, "linguist-vendored", attr.Attribute) 42 46 assert.Equal(t, "unspecified", attr.Value) ··· 51 55 assert.NoError(t, err) 52 56 53 57 select { 54 - case <-wr.ReadAttribute(): 58 + case <-wr.attributes: 55 59 assert.FailNow(t, "There should not be an attribute ready to read") 56 60 case <-time.After(100 * time.Millisecond): 57 61 } 58 62 _, err = wr.Write([]byte("attribute\x00")) 59 63 assert.NoError(t, err) 60 64 select { 61 - case <-wr.ReadAttribute(): 65 + case <-wr.attributes: 62 66 assert.FailNow(t, "There should not be an attribute ready to read") 63 67 case <-time.After(100 * time.Millisecond): 64 68 } ··· 66 70 _, err = wr.Write([]byte("value\x00")) 67 71 assert.NoError(t, err) 68 72 69 - attr := <-wr.ReadAttribute() 73 + attr := <-wr.attributes 70 74 assert.Equal(t, "incomplete-filename", attr.Filename) 71 75 assert.Equal(t, "attribute", attr.Attribute) 72 76 assert.Equal(t, "value", attr.Value) 73 77 74 78 _, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00")) 75 79 assert.NoError(t, err) 76 - attr = <-wr.ReadAttribute() 80 + attr = <-wr.attributes 77 81 assert.NoError(t, err) 78 82 assert.EqualValues(t, attributeTriple{ 79 83 Filename: "shouldbe.vendor", 80 84 Attribute: "linguist-vendored", 81 85 Value: "set", 82 86 }, attr) 83 - attr = <-wr.ReadAttribute() 87 + attr = <-wr.attributes 84 88 assert.NoError(t, err) 85 89 assert.EqualValues(t, attributeTriple{ 86 90 Filename: "shouldbe.vendor", 87 91 Attribute: "linguist-generated", 88 92 Value: "unspecified", 89 93 }, attr) 90 - attr = <-wr.ReadAttribute() 94 + attr = <-wr.attributes 91 95 assert.NoError(t, err) 92 96 assert.EqualValues(t, attributeTriple{ 93 97 Filename: "shouldbe.vendor", ··· 95 99 Value: "unspecified", 96 100 }, attr) 97 101 } 102 + 103 + func TestGitAttributeBareNonBare(t *testing.T) { 104 + if !SupportCheckAttrOnBare { 105 + t.Skip("git check-attr supported on bare repo starting with git 2.40") 106 + } 107 + 108 + repoPath := filepath.Join(testReposDir, "language_stats_repo") 109 + gitRepo, err := openRepositoryWithDefaultContext(repoPath) 110 + require.NoError(t, err) 111 + defer gitRepo.Close() 112 + 113 + for _, commitID := range []string{ 114 + "8fee858da5796dfb37704761701bb8e800ad9ef3", 115 + "341fca5b5ea3de596dc483e54c2db28633cd2f97", 116 + } { 117 + t.Run("GitAttributeChecker/"+commitID, func(t *testing.T) { 118 + bareChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...) 119 + assert.NoError(t, err) 120 + t.Cleanup(func() { bareChecker.Close() }) 121 + 122 + bareStats, err := bareChecker.CheckPath("i-am-a-python.p") 123 + assert.NoError(t, err) 124 + 125 + defer test.MockVariableValue(&SupportCheckAttrOnBare, false)() 126 + cloneChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...) 127 + assert.NoError(t, err) 128 + t.Cleanup(func() { cloneChecker.Close() }) 129 + cloneStats, err := cloneChecker.CheckPath("i-am-a-python.p") 130 + assert.NoError(t, err) 131 + 132 + assert.EqualValues(t, cloneStats, bareStats) 133 + }) 134 + 135 + t.Run("GitAttributes/"+commitID, func(t *testing.T) { 136 + bareStats, err := gitRepo.GitAttributes(commitID, "i-am-a-python.p", LinguistAttributes...) 137 + assert.NoError(t, err) 138 + 139 + defer test.MockVariableValue(&SupportCheckAttrOnBare, false)() 140 + cloneStats, err := gitRepo.GitAttributes(commitID, "i-am-a-python.p", LinguistAttributes...) 141 + assert.NoError(t, err) 142 + 143 + assert.EqualValues(t, cloneStats, bareStats) 144 + }) 145 + } 146 + } 147 + 148 + func TestGitAttributes(t *testing.T) { 149 + repoPath := filepath.Join(testReposDir, "language_stats_repo") 150 + gitRepo, err := openRepositoryWithDefaultContext(repoPath) 151 + require.NoError(t, err) 152 + defer gitRepo.Close() 153 + 154 + attr, err := gitRepo.GitAttributes("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", LinguistAttributes...) 155 + assert.NoError(t, err) 156 + assert.EqualValues(t, map[string]GitAttribute{ 157 + "gitlab-language": "unspecified", 158 + "linguist-detectable": "unspecified", 159 + "linguist-documentation": "unspecified", 160 + "linguist-generated": "unspecified", 161 + "linguist-language": "Python", 162 + "linguist-vendored": "unspecified", 163 + }, attr) 164 + 165 + attr, err = gitRepo.GitAttributes("341fca5b5ea3de596dc483e54c2db28633cd2f97", "i-am-a-python.p", LinguistAttributes...) 166 + assert.NoError(t, err) 167 + assert.EqualValues(t, map[string]GitAttribute{ 168 + "gitlab-language": "unspecified", 169 + "linguist-detectable": "unspecified", 170 + "linguist-documentation": "unspecified", 171 + "linguist-generated": "unspecified", 172 + "linguist-language": "Cobra", 173 + "linguist-vendored": "unspecified", 174 + }, attr) 175 + } 176 + 177 + func TestGitAttributeFirst(t *testing.T) { 178 + repoPath := filepath.Join(testReposDir, "language_stats_repo") 179 + gitRepo, err := openRepositoryWithDefaultContext(repoPath) 180 + require.NoError(t, err) 181 + defer gitRepo.Close() 182 + 183 + t.Run("first is specified", func(t *testing.T) { 184 + language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "linguist-language", "gitlab-language") 185 + assert.NoError(t, err) 186 + assert.Equal(t, "Python", language.String()) 187 + }) 188 + 189 + t.Run("second is specified", func(t *testing.T) { 190 + language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "gitlab-language", "linguist-language") 191 + assert.NoError(t, err) 192 + assert.Equal(t, "Python", language.String()) 193 + }) 194 + 195 + t.Run("none is specified", func(t *testing.T) { 196 + language, err := gitRepo.GitAttributeFirst("8fee858da5796dfb37704761701bb8e800ad9ef3", "i-am-a-python.p", "linguist-detectable", "gitlab-language", "non-existing") 197 + assert.NoError(t, err) 198 + assert.Equal(t, "", language.String()) 199 + }) 200 + } 201 + 202 + func TestGitAttributeStruct(t *testing.T) { 203 + assert.Equal(t, "", GitAttribute("").String()) 204 + assert.Equal(t, "", GitAttribute("unspecified").String()) 205 + 206 + assert.Equal(t, "python", GitAttribute("python").String()) 207 + 208 + assert.Equal(t, "text?token=Error", GitAttribute("text?token=Error").String()) 209 + assert.Equal(t, "text", GitAttribute("text?token=Error").Prefix()) 210 + }
+23 -36
modules/git/repo_language_stats_nogogit.go
··· 8 8 9 9 import ( 10 10 "bytes" 11 + "cmp" 11 12 "io" 12 - "strings" 13 13 14 14 "code.gitea.io/gitea/modules/analyze" 15 15 "code.gitea.io/gitea/modules/log" ··· 61 61 return nil, err 62 62 } 63 63 64 - checker, deferable := repo.CheckAttributeReader(commitID) 65 - defer deferable() 64 + checker, err := repo.GitAttributeChecker(commitID, LinguistAttributes...) 65 + if err != nil { 66 + return nil, err 67 + } 68 + defer checker.Close() 66 69 67 70 contentBuf := bytes.Buffer{} 68 71 var content []byte ··· 102 105 isDocumentation := optional.None[bool]() 103 106 isDetectable := optional.None[bool]() 104 107 105 - if checker != nil { 106 - attrs, err := checker.CheckPath(f.Name()) 107 - if err == nil { 108 - isVendored = attributeToBool(attrs, "linguist-vendored") 109 - isGenerated = attributeToBool(attrs, "linguist-generated") 110 - isDocumentation = attributeToBool(attrs, "linguist-documentation") 111 - isDetectable = attributeToBool(attrs, "linguist-detectable") 112 - if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { 113 - // group languages, such as Pug -> HTML; SCSS -> CSS 114 - group := enry.GetLanguageGroup(language) 115 - if len(group) != 0 { 116 - language = group 117 - } 118 - 119 - // this language will always be added to the size 120 - sizes[language] += f.Size() 121 - continue 122 - } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" { 123 - // strip off a ? if present 124 - if idx := strings.IndexByte(language, '?'); idx >= 0 { 125 - language = language[:idx] 126 - } 127 - if len(language) != 0 { 128 - // group languages, such as Pug -> HTML; SCSS -> CSS 129 - group := enry.GetLanguageGroup(language) 130 - if len(group) != 0 { 131 - language = group 132 - } 133 - 134 - // this language will always be added to the size 135 - sizes[language] += f.Size() 136 - continue 137 - } 108 + attrs, err := checker.CheckPath(f.Name()) 109 + if err == nil { 110 + isVendored = attrs["linguist-vendored"].Bool() 111 + isGenerated = attrs["linguist-generated"].Bool() 112 + isDocumentation = attrs["linguist-documentation"].Bool() 113 + isDetectable = attrs["linguist-detectable"].Bool() 114 + if language := cmp.Or( 115 + attrs["linguist-language"].String(), 116 + attrs["gitlab-language"].Prefix(), 117 + ); language != "" { 118 + // group languages, such as Pug -> HTML; SCSS -> CSS 119 + group := enry.GetLanguageGroup(language) 120 + if len(group) != 0 { 121 + language = group 138 122 } 139 123 124 + // this language will always be added to the size 125 + sizes[language] += f.Size() 126 + continue 140 127 } 141 128 } 142 129
+1 -1
modules/git/tests/repos/language_stats_repo/config
··· 1 1 [core] 2 2 repositoryformatversion = 0 3 3 filemode = true 4 - bare = false 4 + bare = true 5 5 logallrefupdates = true
+1
modules/git/tests/repos/language_stats_repo/logs/HEAD
··· 1 1 0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats 2 + 8fee858da5796dfb37704761701bb8e800ad9ef3 341fca5b5ea3de596dc483e54c2db28633cd2f97 oliverpool <git@olivier.pfad.fr> 1711278775 +0100 push
+1
modules/git/tests/repos/language_stats_repo/logs/refs/heads/master
··· 1 1 0000000000000000000000000000000000000000 8fee858da5796dfb37704761701bb8e800ad9ef3 Andrew Thornton <art27@cantab.net> 1632140318 +0100 commit (initial): Add some test files for GetLanguageStats 2 + 8fee858da5796dfb37704761701bb8e800ad9ef3 341fca5b5ea3de596dc483e54c2db28633cd2f97 oliverpool <git@olivier.pfad.fr> 1711278775 +0100 push
modules/git/tests/repos/language_stats_repo/objects/1e/ea60592b55dcb45c36029cc1202132e9fb756c

This is a binary file and will not be displayed.

modules/git/tests/repos/language_stats_repo/objects/22/b6aa0588563508d8879f062470c8cbc7b2f2bb

This is a binary file and will not be displayed.

modules/git/tests/repos/language_stats_repo/objects/34/1fca5b5ea3de596dc483e54c2db28633cd2f97

This is a binary file and will not be displayed.

+1 -1
modules/git/tests/repos/language_stats_repo/refs/heads/master
··· 1 - 8fee858da5796dfb37704761701bb8e800ad9ef3 1 + 341fca5b5ea3de596dc483e54c2db28633cd2f97
+20 -19
routers/web/repo/setting/lfs.go
··· 145 145 return 146 146 } 147 147 148 - name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ 149 - Attributes: []string{"lockable"}, 150 - Filenames: filenames, 151 - CachedOnly: true, 152 - }) 148 + ctx.Data["Lockables"], err = lockablesGitAttributes(gitRepo, lfsLocks) 153 149 if err != nil { 154 - log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err) 150 + log.Error("Unable to get lockablesGitAttributes in %s (%v)", tmpBasePath, err) 155 151 ctx.ServerError("LFSLocks", err) 156 152 return 157 153 } 158 154 159 - lockables := make([]bool, len(lfsLocks)) 160 - for i, lock := range lfsLocks { 161 - attribute2info, has := name2attribute2info[lock.Path] 162 - if !has { 163 - continue 164 - } 165 - if attribute2info["lockable"] != "set" { 166 - continue 167 - } 168 - lockables[i] = true 169 - } 170 - ctx.Data["Lockables"] = lockables 171 - 172 155 filelist, err := gitRepo.LsFiles(filenames...) 173 156 if err != nil { 174 157 log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err) ··· 187 170 188 171 ctx.Data["Page"] = pager 189 172 ctx.HTML(http.StatusOK, tplSettingsLFSLocks) 173 + } 174 + 175 + func lockablesGitAttributes(gitRepo *git.Repository, lfsLocks []*git_model.LFSLock) ([]bool, error) { 176 + checker, err := gitRepo.GitAttributeChecker("", "lockable") 177 + if err != nil { 178 + return nil, fmt.Errorf("could not GitAttributeChecker: %w", err) 179 + } 180 + defer checker.Close() 181 + 182 + lockables := make([]bool, len(lfsLocks)) 183 + for i, lock := range lfsLocks { 184 + attrs, err := checker.CheckPath(lock.Path) 185 + if err != nil { 186 + return nil, fmt.Errorf("could not CheckPath(%s): %w", lock.Path, err) 187 + } 188 + lockables[i] = attrs["lockable"].Bool().Value() 189 + } 190 + return lockables, nil 190 191 } 191 192 192 193 // LFSLockFile locks a file
+6 -11
routers/web/repo/view.go
··· 643 643 } 644 644 645 645 if ctx.Repo.GitRepo != nil { 646 - checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) 647 - if checker != nil { 648 - defer deferable() 649 - attrs, err := checker.CheckPath(ctx.Repo.TreePath) 650 - if err == nil { 651 - vendored, has := attrs["linguist-vendored"] 652 - ctx.Data["IsVendored"] = has && (vendored == "set" || vendored == "true") 653 - 654 - generated, has := attrs["linguist-generated"] 655 - ctx.Data["IsGenerated"] = has && (generated == "set" || generated == "true") 656 - } 646 + attrs, err := ctx.Repo.GitRepo.GitAttributes(ctx.Repo.CommitID, ctx.Repo.TreePath, "linguist-vendored", "linguist-generated") 647 + if err != nil { 648 + log.Error("GitAttributes(%s, %s) failed: %v", ctx.Repo.CommitID, ctx.Repo.TreePath, err) 649 + } else { 650 + ctx.Data["IsVendored"] = attrs["linguist-vendored"].Bool().Value() 651 + ctx.Data["IsGenerated"] = attrs["linguist-generated"].Bool().Value() 657 652 } 658 653 } 659 654
+23 -28
services/gitdiff/gitdiff.go
··· 7 7 import ( 8 8 "bufio" 9 9 "bytes" 10 + "cmp" 10 11 "context" 11 12 "fmt" 12 13 "html" ··· 1172 1173 } 1173 1174 diff.Start = opts.SkipTo 1174 1175 1175 - checker, deferable := gitRepo.CheckAttributeReader(opts.AfterCommitID) 1176 - defer deferable() 1176 + checker, err := gitRepo.GitAttributeChecker(opts.AfterCommitID, git.LinguistAttributes...) 1177 + if err != nil { 1178 + return nil, fmt.Errorf("unable to GitAttributeChecker: %w", err) 1179 + } 1180 + defer checker.Close() 1177 1181 1178 1182 for _, diffFile := range diff.Files { 1179 - 1180 1183 gotVendor := false 1181 1184 gotGenerated := false 1182 - if checker != nil { 1183 - attrs, err := checker.CheckPath(diffFile.Name) 1184 - if err == nil { 1185 - if vendored, has := attrs["linguist-vendored"]; has { 1186 - if vendored == "set" || vendored == "true" { 1187 - diffFile.IsVendored = true 1188 - gotVendor = true 1189 - } else { 1190 - gotVendor = vendored == "false" 1191 - } 1192 - } 1193 - if generated, has := attrs["linguist-generated"]; has { 1194 - if generated == "set" || generated == "true" { 1195 - diffFile.IsGenerated = true 1196 - gotGenerated = true 1197 - } else { 1198 - gotGenerated = generated == "false" 1199 - } 1200 - } 1201 - if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { 1202 - diffFile.Language = language 1203 - } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" { 1204 - diffFile.Language = language 1205 - } 1206 - } 1185 + 1186 + attrs, err := checker.CheckPath(diffFile.Name) 1187 + if err != nil { 1188 + log.Error("checker.CheckPath(%s) failed: %v", diffFile.Name, err) 1189 + } else { 1190 + vendored := attrs["linguist-vendored"].Bool() 1191 + diffFile.IsVendored = vendored.Value() 1192 + gotVendor = vendored.Has() 1193 + 1194 + generated := attrs["linguist-generated"].Bool() 1195 + diffFile.IsGenerated = generated.Value() 1196 + gotGenerated = generated.Has() 1197 + 1198 + diffFile.Language = cmp.Or( 1199 + attrs["linguist-language"].String(), 1200 + attrs["gitlab-language"].Prefix(), 1201 + ) 1207 1202 } 1208 1203 1209 1204 if !gotVendor {
+2 -27
services/repository/files/content.go
··· 273 273 274 274 // TryGetContentLanguage tries to get the (linguist) language of the file content 275 275 func TryGetContentLanguage(gitRepo *git.Repository, commitID, treePath string) (string, error) { 276 - indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(commitID) 277 - if err != nil { 278 - return "", err 279 - } 280 - 281 - defer deleteTemporaryFile() 282 - 283 - filename2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{ 284 - CachedOnly: true, 285 - Attributes: []string{"linguist-language", "gitlab-language"}, 286 - Filenames: []string{treePath}, 287 - IndexFile: indexFilename, 288 - WorkTree: worktree, 289 - }) 290 - if err != nil { 291 - return "", err 292 - } 293 - 294 - language := filename2attribute2info[treePath]["linguist-language"] 295 - if language == "" || language == "unspecified" { 296 - language = filename2attribute2info[treePath]["gitlab-language"] 297 - } 298 - if language == "unspecified" { 299 - language = "" 300 - } 301 - 302 - return language, nil 276 + attribute, err := gitRepo.GitAttributeFirst(commitID, treePath, "linguist-language", "gitlab-language") 277 + return attribute.Prefix(), err 303 278 }
+2 -6
services/repository/files/update.go
··· 400 400 var lfsMetaObject *git_model.LFSMetaObject 401 401 if setting.LFS.StartServer && hasOldBranch { 402 402 // Check there is no way this can return multiple infos 403 - filename2attribute2info, err := t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ 404 - Attributes: []string{"filter"}, 405 - Filenames: []string{file.Options.treePath}, 406 - CachedOnly: true, 407 - }) 403 + filterAttribute, err := t.gitRepo.GitAttributeFirst("", file.Options.treePath, "filter") 408 404 if err != nil { 409 405 return err 410 406 } 411 407 412 - if filename2attribute2info[file.Options.treePath] != nil && filename2attribute2info[file.Options.treePath]["filter"] == "lfs" { 408 + if filterAttribute == "lfs" { 413 409 // OK so we are supposed to LFS this data! 414 410 pointer, err := lfs.GeneratePointer(treeObjectContentReader) 415 411 if err != nil {
+41 -18
services/repository/files/upload.go
··· 105 105 } 106 106 } 107 107 108 - var filename2attribute2info map[string]map[string]string 109 - if setting.LFS.StartServer { 110 - filename2attribute2info, err = t.gitRepo.CheckAttribute(git.CheckAttributeOpts{ 111 - Attributes: []string{"filter"}, 112 - Filenames: names, 113 - CachedOnly: true, 114 - }) 115 - if err != nil { 116 - return err 117 - } 118 - } 119 - 120 108 // Copy uploaded files into repository. 121 - for i := range infos { 122 - if err := copyUploadedLFSFileIntoRepository(&infos[i], filename2attribute2info, t, opts.TreePath); err != nil { 123 - return err 124 - } 109 + if err := copyUploadedLFSFilesIntoRepository(infos, t, opts.TreePath); err != nil { 110 + return err 125 111 } 126 112 127 113 // Now write the tree ··· 169 155 return repo_model.DeleteUploads(ctx, uploads...) 170 156 } 171 157 172 - func copyUploadedLFSFileIntoRepository(info *uploadInfo, filename2attribute2info map[string]map[string]string, t *TemporaryUploadRepository, treePath string) error { 158 + func copyUploadedLFSFilesIntoRepository(infos []uploadInfo, t *TemporaryUploadRepository, treePath string) error { 159 + var storeInLFSFunc func(string) (bool, error) 160 + 161 + if setting.LFS.StartServer { 162 + checker, err := t.gitRepo.GitAttributeChecker("", "filter") 163 + if err != nil { 164 + return err 165 + } 166 + defer checker.Close() 167 + 168 + storeInLFSFunc = func(name string) (bool, error) { 169 + attrs, err := checker.CheckPath(name) 170 + if err != nil { 171 + return false, fmt.Errorf("could not CheckPath(%s): %w", name, err) 172 + } 173 + return attrs["filter"] == "lfs", nil 174 + } 175 + } 176 + 177 + // Copy uploaded files into repository. 178 + for i, info := range infos { 179 + storeInLFS := false 180 + if storeInLFSFunc != nil { 181 + var err error 182 + storeInLFS, err = storeInLFSFunc(info.upload.Name) 183 + if err != nil { 184 + return err 185 + } 186 + } 187 + 188 + if err := copyUploadedLFSFileIntoRepository(&infos[i], storeInLFS, t, treePath); err != nil { 189 + return err 190 + } 191 + } 192 + return nil 193 + } 194 + 195 + func copyUploadedLFSFileIntoRepository(info *uploadInfo, storeInLFS bool, t *TemporaryUploadRepository, treePath string) error { 173 196 file, err := os.Open(info.upload.LocalPath()) 174 197 if err != nil { 175 198 return err ··· 177 200 defer file.Close() 178 201 179 202 var objectHash string 180 - if setting.LFS.StartServer && filename2attribute2info[info.upload.Name] != nil && filename2attribute2info[info.upload.Name]["filter"] == "lfs" { 203 + if storeInLFS { 181 204 // Handle LFS 182 205 // FIXME: Inefficient! this should probably happen in models.Upload 183 206 pointer, err := lfs.GeneratePointer(file)