|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "bytes" |
5 | | - "context" |
6 | | - "fmt" |
7 | | - "os" |
8 | | - "os/exec" |
9 | | - "path/filepath" |
10 | | - "regexp" |
11 | | - "strings" |
12 | | - "time" |
| 4 | + "bytes" |
| 5 | + "context" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + "os/exec" |
| 9 | + "path/filepath" |
| 10 | + "regexp" |
| 11 | + "sort" |
| 12 | + "strings" |
| 13 | + "time" |
13 | 14 | ) |
14 | 15 |
|
15 | 16 | // App struct |
@@ -119,45 +120,77 @@ func (a *App) GenerateRepo(req GenerateRepoRequest) (*GenerateRepoResponse, erro |
119 | 120 | return nil, err |
120 | 121 | } |
121 | 122 |
|
122 | | - activityFileName := "activity.log" |
123 | | - activityFilePath := filepath.Join(repoPath, activityFileName) |
124 | | - |
125 | | - totalCommits := 0 |
126 | | - for _, day := range req.Contributions { |
127 | | - if day.Count <= 0 { |
128 | | - continue |
129 | | - } |
130 | | - parsedDate, err := time.Parse("2006-01-02", day.Date) |
131 | | - if err != nil { |
132 | | - return nil, fmt.Errorf("invalid date %q: %w", day.Date, err) |
133 | | - } |
134 | | - |
135 | | - for i := 0; i < day.Count; i++ { |
136 | | - entry := fmt.Sprintf("%s commit %d\n", day.Date, i+1) |
137 | | - if err := appendToFile(activityFilePath, entry); err != nil { |
138 | | - return nil, fmt.Errorf("update activity log: %w", err) |
139 | | - } |
140 | | - |
141 | | - if err := runGitCommand(repoPath, "add", activityFileName, filepath.Base(readmePath)); err != nil { |
142 | | - return nil, err |
143 | | - } |
144 | | - |
145 | | - commitTime := parsedDate.Add(time.Duration(i) * time.Second) |
146 | | - env := map[string]string{ |
147 | | - "GIT_AUTHOR_NAME": username, |
148 | | - "GIT_AUTHOR_EMAIL": email, |
149 | | - "GIT_COMMITTER_NAME": username, |
150 | | - "GIT_COMMITTER_EMAIL": email, |
151 | | - "GIT_AUTHOR_DATE": commitTime.Format(time.RFC3339), |
152 | | - "GIT_COMMITTER_DATE": commitTime.Format(time.RFC3339), |
153 | | - } |
154 | | - message := fmt.Sprintf("Contribution on %s (%d/%d)", day.Date, i+1, day.Count) |
155 | | - if err := runGitCommandWithEnv(repoPath, env, "commit", "-m", message); err != nil { |
156 | | - return nil, err |
157 | | - } |
158 | | - totalCommits++ |
159 | | - } |
160 | | - } |
| 123 | + // Optimize: use git fast-import to avoid spawning a process per commit. |
| 124 | + // Also disable slow features for this repo. |
| 125 | + _ = runGitCommand(repoPath, "config", "commit.gpgsign", "false") |
| 126 | + _ = runGitCommand(repoPath, "config", "gc.auto", "0") |
| 127 | + _ = runGitCommand(repoPath, "config", "core.autocrlf", "false") |
| 128 | + _ = runGitCommand(repoPath, "config", "core.fsyncObjectFiles", "false") |
| 129 | + |
| 130 | + // Sort contributions by date ascending to produce chronological history |
| 131 | + contribs := make([]ContributionDay, 0, len(req.Contributions)) |
| 132 | + for _, c := range req.Contributions { |
| 133 | + if c.Count > 0 { |
| 134 | + contribs = append(contribs, c) |
| 135 | + } |
| 136 | + } |
| 137 | + sort.Slice(contribs, func(i, j int) bool { return contribs[i].Date < contribs[j].Date }) |
| 138 | + |
| 139 | + // Build fast-import stream |
| 140 | + var stream bytes.Buffer |
| 141 | + // Create README blob once and mark it |
| 142 | + fmt.Fprintf(&stream, "blob\nmark :1\n") |
| 143 | + fmt.Fprintf(&stream, "data %d\n%s\n", len(readmeContent), readmeContent) |
| 144 | + |
| 145 | + // Prepare to accumulate activity log content across commits |
| 146 | + var activityBuf bytes.Buffer |
| 147 | + nextMark := 2 |
| 148 | + totalCommits := 0 |
| 149 | + branch := "refs/heads/main" |
| 150 | + |
| 151 | + for _, day := range contribs { |
| 152 | + parsedDate, err := time.Parse("2006-01-02", day.Date) |
| 153 | + if err != nil { |
| 154 | + return nil, fmt.Errorf("invalid date %q: %w", day.Date, err) |
| 155 | + } |
| 156 | + for i := 0; i < day.Count; i++ { |
| 157 | + // Update activity content in-memory |
| 158 | + entry := fmt.Sprintf("%s commit %d\n", day.Date, i+1) |
| 159 | + activityBuf.WriteString(entry) |
| 160 | + |
| 161 | + // Emit blob for activity.log |
| 162 | + fmt.Fprintf(&stream, "blob\nmark :%d\n", nextMark) |
| 163 | + act := activityBuf.Bytes() |
| 164 | + fmt.Fprintf(&stream, "data %d\n", len(act)) |
| 165 | + stream.Write(act) |
| 166 | + stream.WriteString("\n") |
| 167 | + |
| 168 | + // Emit commit that points to README (:1) and activity (:nextMark) |
| 169 | + commitTime := parsedDate.Add(time.Duration(i) * time.Second) |
| 170 | + secs := commitTime.Unix() |
| 171 | + tz := commitTime.Format("-0700") |
| 172 | + msg := fmt.Sprintf("Contribution on %s (%d/%d)", day.Date, i+1, day.Count) |
| 173 | + fmt.Fprintf(&stream, "commit %s\n", branch) |
| 174 | + fmt.Fprintf(&stream, "author %s <%s> %d %s\n", username, email, secs, tz) |
| 175 | + fmt.Fprintf(&stream, "committer %s <%s> %d %s\n", username, email, secs, tz) |
| 176 | + fmt.Fprintf(&stream, "data %d\n%s\n", len(msg), msg) |
| 177 | + fmt.Fprintf(&stream, "M 100644 :1 %s\n", filepath.Base(readmePath)) |
| 178 | + fmt.Fprintf(&stream, "M 100644 :%d activity.log\n", nextMark) |
| 179 | + |
| 180 | + nextMark++ |
| 181 | + totalCommits++ |
| 182 | + } |
| 183 | + } |
| 184 | + stream.WriteString("done\n") |
| 185 | + |
| 186 | + // Feed stream to fast-import |
| 187 | + if totalCommits > 0 { |
| 188 | + if err := runGitFastImport(repoPath, &stream); err != nil { |
| 189 | + return nil, fmt.Errorf("fast-import failed: %w", err) |
| 190 | + } |
| 191 | + // Update working tree to the generated branch for user convenience |
| 192 | + _ = runGitCommand(repoPath, "checkout", "-f", "main") |
| 193 | + } |
161 | 194 |
|
162 | 195 | if err := openDirectory(repoPath); err != nil { |
163 | 196 | return nil, fmt.Errorf("open repo directory: %w", err) |
@@ -233,3 +266,17 @@ func runGitCommandWithEnv(dir string, extraEnv map[string]string, args ...string |
233 | 266 |
|
234 | 267 | return nil |
235 | 268 | } |
| 269 | + |
| 270 | +// runGitFastImport runs `git fast-import` with the given stream as stdin. |
| 271 | +func runGitFastImport(dir string, r *bytes.Buffer) error { |
| 272 | + cmd := exec.Command("git", "fast-import", "--quiet") |
| 273 | + cmd.Dir = dir |
| 274 | + configureCommand(cmd, true) |
| 275 | + cmd.Stdin = r |
| 276 | + var stderr bytes.Buffer |
| 277 | + cmd.Stderr = &stderr |
| 278 | + if err := cmd.Run(); err != nil { |
| 279 | + return fmt.Errorf("git fast-import: %w (%s)", err, strings.TrimSpace(stderr.String())) |
| 280 | + } |
| 281 | + return nil |
| 282 | +} |
0 commit comments