forked from tangled.org/core
Monorepo for Tangled

Compare changes

Choose any two refs to compare.

+758 -338
-24
appview/db/db.go
··· 1181 1181 return err 1182 1182 }) 1183 1183 1184 - orm.RunMigration(conn, logger, "remove-profile-stats-column-constraint", func(tx *sql.Tx) error { 1185 - _, err := tx.Exec(` 1186 - -- create new table without the check constraint 1187 - create table profile_stats_new ( 1188 - id integer primary key autoincrement, 1189 - did text not null, 1190 - kind text not null, -- no constraint this time 1191 - foreign key (did) references profile(did) on delete cascade 1192 - ); 1193 - 1194 - -- copy data from old table 1195 - insert into profile_stats_new (id, did, kind) 1196 - select id, did, kind 1197 - from profile_stats; 1198 - 1199 - -- drop old table 1200 - drop table profile_stats; 1201 - 1202 - -- rename new table 1203 - alter table profile_stats_new rename to profile_stats; 1204 - `) 1205 - return err 1206 - }) 1207 - 1208 1184 return &DB{ 1209 1185 db, 1210 1186 logger,
-5
appview/db/profile.go
··· 450 450 case models.VanityStatRepositoryCount: 451 451 query = `select count(id) from repos where did = ?` 452 452 args = append(args, did) 453 - case models.VanityStatStarCount: 454 - query = `select count(id) from stars where subject_at like 'at://' || ? || '%'` 455 - args = append(args, did) 456 - default: 457 - return 0, fmt.Errorf("invalid vanity stat kind: %s", stat) 458 453 } 459 454 460 455 var result uint64
-3
appview/models/profile.go
··· 59 59 VanityStatOpenIssueCount VanityStatKind = "open-issue-count" 60 60 VanityStatClosedIssueCount VanityStatKind = "closed-issue-count" 61 61 VanityStatRepositoryCount VanityStatKind = "repository-count" 62 - VanityStatStarCount VanityStatKind = "star-count" 63 62 ) 64 63 65 64 func (v VanityStatKind) String() string { ··· 76 75 return "Closed Issues" 77 76 case VanityStatRepositoryCount: 78 77 return "Repositories" 79 - case VanityStatStarCount: 80 - return "Stars Received" 81 78 } 82 79 return "" 83 80 }
-113
appview/pages/templates/fragments/resizeable.html
··· 1 - {{ define "fragments/resizable" }} 2 - <script> 3 - class ResizablePanel { 4 - constructor(resizerElement) { 5 - this.resizer = resizerElement; 6 - this.isResizing = false; 7 - this.type = resizerElement.dataset.resizer; 8 - this.targetId = resizerElement.dataset.target; 9 - this.target = document.getElementById(this.targetId); 10 - this.min = parseInt(resizerElement.dataset.min) || 100; 11 - this.max = parseInt(resizerElement.dataset.max) || Infinity; 12 - 13 - this.direction = resizerElement.dataset.direction || 'before'; // 'before' or 'after' 14 - 15 - this.handleMouseDown = this.handleMouseDown.bind(this); 16 - this.handleMouseMove = this.handleMouseMove.bind(this); 17 - this.handleMouseUp = this.handleMouseUp.bind(this); 18 - 19 - this.init(); 20 - } 21 - 22 - init() { 23 - this.resizer.addEventListener('mousedown', this.handleMouseDown); 24 - } 25 - 26 - handleMouseDown(e) { 27 - e.preventDefault(); 28 - this.isResizing = true; 29 - this.resizer.classList.add('resizing'); 30 - document.body.style.cursor = this.type === 'vertical' ? 'col-resize' : 'row-resize'; 31 - document.body.style.userSelect = 'none'; 32 - 33 - this.startX = e.clientX; 34 - this.startY = e.clientY; 35 - this.startWidth = this.target.offsetWidth; 36 - this.startHeight = this.target.offsetHeight; 37 - 38 - document.addEventListener('mousemove', this.handleMouseMove); 39 - document.addEventListener('mouseup', this.handleMouseUp); 40 - } 41 - 42 - handleMouseMove(e) { 43 - if (!this.isResizing) return; 44 - 45 - if (this.type === 'vertical') { 46 - let newWidth; 47 - 48 - if (this.direction === 'after') { 49 - const deltaX = this.startX - e.clientX; 50 - newWidth = this.startWidth + deltaX; 51 - } else { 52 - const deltaX = e.clientX - this.startX; 53 - newWidth = this.startWidth + deltaX; 54 - } 55 - 56 - if (newWidth >= this.min && newWidth <= this.max) { 57 - this.target.style.width = newWidth + 'px'; 58 - this.target.style.flexShrink = '0'; 59 - } 60 - } else { 61 - let newHeight; 62 - 63 - if (this.direction === 'after') { 64 - const deltaY = this.startY - e.clientY; 65 - newHeight = this.startHeight + deltaY; 66 - } else { 67 - const deltaY = e.clientY - this.startY; 68 - newHeight = this.startHeight + deltaY; 69 - } 70 - 71 - if (newHeight >= this.min && newHeight <= this.max) { 72 - this.target.style.height = newHeight + 'px'; 73 - } 74 - } 75 - } 76 - 77 - handleMouseUp() { 78 - if (!this.isResizing) return; 79 - 80 - this.isResizing = false; 81 - this.resizer.classList.remove('resizing'); 82 - document.body.style.cursor = ''; 83 - document.body.style.userSelect = ''; 84 - 85 - document.removeEventListener('mousemove', this.handleMouseMove); 86 - document.removeEventListener('mouseup', this.handleMouseUp); 87 - } 88 - 89 - destroy() { 90 - this.resizer.removeEventListener('mousedown', this.handleMouseDown); 91 - document.removeEventListener('mousemove', this.handleMouseMove); 92 - document.removeEventListener('mouseup', this.handleMouseUp); 93 - } 94 - } 95 - 96 - function initializeResizers() { 97 - const resizers = document.querySelectorAll('[data-resizer]'); 98 - const instances = []; 99 - 100 - resizers.forEach(resizer => { 101 - instances.push(new ResizablePanel(resizer)); 102 - }); 103 - 104 - return instances; 105 - } 106 - 107 - if (document.readyState === 'loading') { 108 - document.addEventListener('DOMContentLoaded', initializeResizers); 109 - } else { 110 - initializeResizers(); 111 - } 112 - </script> 113 - {{ end }}
+3 -3
appview/pages/templates/fragments/starBtn.html
··· 15 15 hx-disabled-elt="#starBtn" 16 16 > 17 17 {{ if .IsStarred }} 18 - {{ i "star" "w-4 h-4 fill-current inline group-[.htmx-request]:hidden" }} 18 + {{ i "star" "w-4 h-4 fill-current" }} 19 19 {{ else }} 20 - {{ i "star" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 20 + {{ i "star" "w-4 h-4" }} 21 21 {{ end }} 22 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 23 22 <span class="text-sm"> 24 23 {{ .StarCount }} 25 24 </span> 25 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 26 26 </button> 27 27 {{ end }}
+2 -19
appview/pages/templates/repo/fragments/diff.html
··· 3 3 #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } 4 4 #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 5 #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 - #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; } 6 + #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } 7 7 #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } 8 - #filesToggle:not(:checked) ~ div div#resize-files { display: none; } 9 8 </style> 10 9 11 10 {{ template "diffTopbar" . }} 12 11 {{ block "diffLayout" . }} {{ end }} 13 - {{ template "fragments/resizable" }} 14 12 {{ end }} 15 13 16 14 {{ define "diffTopbar" }} ··· 80 78 81 79 {{ end }} 82 80 83 - {{ define "resize-grip" }} 84 - {{ $id := index . 0 }} 85 - {{ $target := index . 1 }} 86 - {{ $direction := index . 2 }} 87 - <div id="{{ $id }}" 88 - data-resizer="vertical" 89 - data-target="{{ $target }}" 90 - data-direction="{{ $direction }}" 91 - class="resizer-vertical hidden md:flex w-4 sticky top-12 max-h-screen flex-col items-center justify-center group"> 92 - <div class="w-1 h-16 group-hover:h-24 group-[.resizing]:h-24 transition-all rounded-full bg-gray-400 dark:bg-gray-500 group-hover:bg-gray-500 group-hover:dark:bg-gray-400"></div> 93 - </div> 94 - {{ end }} 95 - 96 81 {{ define "diffLayout" }} 97 82 {{ $diff := index . 0 }} 98 83 {{ $opts := index . 1 }} ··· 105 90 </section> 106 91 </div> 107 92 108 - {{ template "resize-grip" (list "resize-files" "files" "before") }} 109 - 110 93 <!-- main content --> 111 - <div id="diff-files" class="flex-1 min-w-0 sticky top-12 pb-12"> 94 + <div class="flex-1 min-w-0 sticky top-12 pb-12"> 112 95 {{ template "diffFiles" (list $diff $opts) }} 113 96 </div> 114 97
+2 -2
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 38 38 hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 39 39 hx-swap="none" 40 40 class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 41 - {{ i "git-branch" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 41 + {{ i "git-branch" "w-4 h-4" }} 42 + <span>delete branch</span> 42 43 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 43 - delete branch 44 44 </button> 45 45 {{ end }} 46 46 {{ if and $isPushAllowed $isOpen $isLastRound }}
+5 -22
appview/pages/templates/repo/pulls/pull.html
··· 111 111 {{ end }} 112 112 {{ end }} 113 113 114 - {{ define "resize-grip" }} 115 - {{ $id := index . 0 }} 116 - {{ $target := index . 1 }} 117 - {{ $direction := index . 2 }} 118 - <div id="{{ $id }}" 119 - data-resizer="vertical" 120 - data-target="{{ $target }}" 121 - data-direction="{{ $direction }}" 122 - class="resizer-vertical hidden md:flex w-4 sticky top-12 max-h-screen flex-col items-center justify-center group"> 123 - <div class="w-1 h-16 group-hover:h-24 group-[.resizing]:h-24 transition-all rounded-full bg-gray-400 dark:bg-gray-500 group-hover:bg-gray-500 group-hover:dark:bg-gray-400"></div> 124 - </div> 125 - {{ end }} 126 - 127 114 {{ define "diffLayout" }} 128 115 {{ $diff := index . 0 }} 129 116 {{ $opts := index . 1 }} ··· 137 124 </section> 138 125 </div> 139 126 140 - {{ template "resize-grip" (list "resize-files" "files" "before") }} 141 - 142 127 <!-- main content --> 143 - <div id="diff-files" class="flex-1 min-w-0 sticky top-12 pb-12"> 128 + <div class="flex-1 min-w-0 sticky top-12 pb-12"> 144 129 {{ template "diffFiles" (list $diff $opts) }} 145 130 </div> 146 - 147 - {{ template "resize-grip" (list "resize-subs" "subs" "after") }} 148 131 149 132 <!-- right panel --> 150 133 {{ template "subsPanel" $ }} ··· 204 187 205 188 {{ define "subsToggle" }} 206 189 <style> 190 + /* Mobile: full width */ 207 191 #subsToggle:checked ~ div div#subs { 208 192 width: 100%; 209 193 margin-left: 0; ··· 212 196 #subsToggle:checked ~ div label[for="subsToggle"] .hide-toggle { display: flex; } 213 197 #subsToggle:not(:checked) ~ div label[for="subsToggle"] .hide-toggle { display: none; } 214 198 199 + /* Desktop: 25vw with left margin */ 215 200 @media (min-width: 768px) { 216 201 #subsToggle:checked ~ div div#subs { 217 202 width: 25vw; 218 - max-width: 50vw; 203 + margin-left: 1rem; 219 204 } 205 + /* Unchecked state */ 220 206 #subsToggle:not(:checked) ~ div div#subs { 221 207 width: 0; 222 208 display: none; 223 209 margin-left: 0; 224 - } 225 - #subsToggle:not(:checked) ~ div div#resize-subs { 226 - display: none; 227 210 } 228 211 } 229 212 </style>
-1
appview/pages/templates/user/fragments/editBio.html
··· 118 118 "open-issue-count" "Open Issue Count" 119 119 "closed-issue-count" "Closed Issue Count" 120 120 "repository-count" "Repository Count" 121 - "star-count" "Star Count" 122 121 }} 123 122 {{ range $s := $stats }} 124 123 {{ $value := index $s 0 }}
+3 -6
appview/pages/templates/user/fragments/follow.html
··· 13 13 hx-swap="outerHTML" 14 14 > 15 15 {{ if eq .FollowStatus.String "IsNotFollowing" }} 16 - {{ i "user-round-plus" "size-4 inline group-[.htmx-request]:hidden" }} 17 - {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 18 - follow 16 + {{ i "user-round-plus" "w-4 h-4" }} follow 19 17 {{ else }} 20 - {{ i "user-round-minus" "size-4 inline group-[.htmx-request]:hidden" }} 21 - {{ i "loader-circle" "size-4 animate-spin hidden group-[.htmx-request]:inline" }} 22 - unfollow 18 + {{ i "user-round-minus" "w-4 h-4" }} unfollow 23 19 {{ end }} 20 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 24 21 </button> 25 22 {{ end }}
-83
docs/DOCS.md
··· 502 502 Note that you should add a newline at the end if setting a non-empty message 503 503 since the knot won't do this for you. 504 504 505 - ## Troubleshooting 506 - 507 - If you run your own knot, you may run into some of these 508 - common issues. You can always join the 509 - [IRC](https://web.libera.chat/#tangled) or 510 - [Discord](https://chat.tangled.org/) if this section does 511 - not help. 512 - 513 - ### Unable to push 514 - 515 - If you are unable to push to your knot or repository: 516 - 517 - 1. First, ensure that you have added your SSH public key to 518 - your account 519 - 2. Check to see that your knot has synced the key by running 520 - `knot keys` 521 - 3. Check to see if git is supplying the correct private key 522 - when pushing: `GIT_SSH_COMMAND="ssh -v" git push ...` 523 - 4. Check to see if `sshd` on the knot is rejecting the push 524 - for some reason: `journalctl -xeu ssh` (or `sshd`, 525 - depending on your machine). These logs are unavailable if 526 - using docker. 527 - 5. Check to see if the knot itself is rejecting the push, 528 - depending on your setup, the logs might be in one of the 529 - following paths: 530 - * `/tmp/knotguard.log` 531 - * `/home/git/log` 532 - * `/home/git/guard.log` 533 - 534 505 # Spindles 535 506 536 507 ## Pipelines ··· 1590 1561 Refer to the [jujutsu 1591 1562 documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers) 1592 1563 for more information. 1593 - 1594 - # Troubleshooting guide 1595 - 1596 - ## Login issues 1597 - 1598 - Owing to the distributed nature of OAuth on AT Protocol, you 1599 - may run into issues with logging in. If you run a 1600 - self-hosted PDS: 1601 - 1602 - - You may need to ensure that your PDS is timesynced using 1603 - NTP: 1604 - * Enable the `ntpd` service 1605 - * Run `ntpd -qg` to synchronize your clock 1606 - - You may need to increase the default request timeout: 1607 - `NODE_OPTIONS="--network-family-autoselection-attempt-timeout=500"` 1608 - 1609 - ## Empty punchcard 1610 - 1611 - For Tangled to register commits that you make across the 1612 - network, you need to setup one of following: 1613 - 1614 - - The committer email should be a verified email associated 1615 - to your account. You can add and verify emails on the 1616 - settings page. 1617 - - Or, the committer email should be set to your account's 1618 - DID: `git config user.email "did:plc:foobar". You can find 1619 - your account's DID on the settings page 1620 - 1621 - ## Commit is not marked as verified 1622 - 1623 - Presently, Tangled only supports SSH commit signatures. 1624 - 1625 - To sign commits using an SSH key with git: 1626 - 1627 - ``` 1628 - git config --global gpg.format ssh 1629 - git config --global user.signingkey ~/.ssh/tangled-key 1630 - ``` 1631 - 1632 - To sign commits using an SSH key with jj, add this to your 1633 - config: 1634 - 1635 - ``` 1636 - [signing] 1637 - behavior = "own" 1638 - backend = "ssh" 1639 - key = "~/.ssh/tangled-key" 1640 - ``` 1641 - 1642 - ## Self-hosted knot issues 1643 - 1644 - If you need help troubleshooting a self-hosted knot, check 1645 - out the [knot troubleshooting 1646 - guide](/knot-self-hosting-guide.html#troubleshooting).
+29 -29
knotserver/git/merge.go
··· 107 107 return fmt.Sprintf("merge failed: %s", e.Message) 108 108 } 109 109 110 - func (g *GitRepo) createTempFileWithPatch(patchData string) (string, error) { 110 + func createTempFileWithPatch(patchData string) (string, error) { 111 111 tmpFile, err := os.CreateTemp("", "git-patch-*.patch") 112 112 if err != nil { 113 113 return "", fmt.Errorf("failed to create temporary patch file: %w", err) ··· 127 127 return tmpFile.Name(), nil 128 128 } 129 129 130 - func (g *GitRepo) cloneRepository(targetBranch string) (string, error) { 130 + func (g *GitRepo) cloneTemp(targetBranch string) (string, error) { 131 131 tmpDir, err := os.MkdirTemp("", "git-clone-") 132 132 if err != nil { 133 133 return "", fmt.Errorf("failed to create temporary directory: %w", err) ··· 147 147 return tmpDir, nil 148 148 } 149 149 150 - func (g *GitRepo) checkPatch(tmpDir, patchFile string) error { 151 - var stderr bytes.Buffer 152 - 153 - cmd := exec.Command("git", "-C", tmpDir, "apply", "--check", "-v", patchFile) 154 - cmd.Stderr = &stderr 155 - 156 - if err := cmd.Run(); err != nil { 157 - conflicts := parseGitApplyErrors(stderr.String()) 158 - return &ErrMerge{ 159 - Message: "patch cannot be applied cleanly", 160 - Conflicts: conflicts, 161 - HasConflict: len(conflicts) > 0, 162 - OtherError: err, 163 - } 164 - } 165 - return nil 166 - } 167 - 168 150 func (g *GitRepo) applyPatch(patchData, patchFile string, opts MergeOptions) error { 169 151 var stderr bytes.Buffer 170 152 var cmd *exec.Cmd ··· 173 155 exec.Command("git", "-C", g.path, "config", "user.name", opts.CommitterName).Run() 174 156 exec.Command("git", "-C", g.path, "config", "user.email", opts.CommitterEmail).Run() 175 157 exec.Command("git", "-C", g.path, "config", "advice.mergeConflict", "false").Run() 158 + exec.Command("git", "-C", g.path, "config", "advice.amWorkDir", "false").Run() 176 159 177 160 // if patch is a format-patch, apply using 'git am' 178 161 if opts.FormatPatch { ··· 213 196 cmd.Stderr = &stderr 214 197 215 198 if err := cmd.Run(); err != nil { 216 - return fmt.Errorf("patch application failed: %s", stderr.String()) 199 + conflicts := parseGitApplyErrors(stderr.String()) 200 + return &ErrMerge{ 201 + Message: "patch cannot be applied cleanly", 202 + Conflicts: conflicts, 203 + HasConflict: len(conflicts) > 0, 204 + OtherError: err, 205 + } 217 206 } 218 207 219 208 return nil ··· 241 230 } 242 231 243 232 func (g *GitRepo) applySingleMailbox(singlePatch types.FormatPatch) (plumbing.Hash, error) { 244 - tmpPatch, err := g.createTempFileWithPatch(singlePatch.Raw) 233 + tmpPatch, err := createTempFileWithPatch(singlePatch.Raw) 245 234 if err != nil { 246 235 return plumbing.ZeroHash, fmt.Errorf("failed to create temporary patch file for singluar mailbox patch: %w", err) 247 236 } ··· 257 246 log.Println("head before apply", head.Hash().String()) 258 247 259 248 if err := cmd.Run(); err != nil { 260 - return plumbing.ZeroHash, fmt.Errorf("patch application failed: %s", stderr.String()) 249 + conflicts := parseGitApplyErrors(stderr.String()) 250 + return plumbing.ZeroHash, &ErrMerge{ 251 + Message: "patch cannot be applied cleanly", 252 + Conflicts: conflicts, 253 + HasConflict: len(conflicts) > 0, 254 + OtherError: err, 255 + } 261 256 } 262 257 263 258 if err := g.Refresh(); err != nil { ··· 324 319 return newHash, nil 325 320 } 326 321 327 - func (g *GitRepo) MergeCheck(patchData string, targetBranch string) error { 322 + func (g *GitRepo) MergeCheckWithOptions(patchData string, targetBranch string, mo MergeOptions) error { 328 323 if val, ok := mergeCheckCache.Get(g, patchData, targetBranch); ok { 329 324 return val 330 325 } 331 326 332 - patchFile, err := g.createTempFileWithPatch(patchData) 327 + patchFile, err := createTempFileWithPatch(patchData) 333 328 if err != nil { 334 329 return &ErrMerge{ 335 330 Message: err.Error(), ··· 338 333 } 339 334 defer os.Remove(patchFile) 340 335 341 - tmpDir, err := g.cloneRepository(targetBranch) 336 + tmpDir, err := g.cloneTemp(targetBranch) 342 337 if err != nil { 343 338 return &ErrMerge{ 344 339 Message: err.Error(), ··· 347 342 } 348 343 defer os.RemoveAll(tmpDir) 349 344 350 - result := g.checkPatch(tmpDir, patchFile) 345 + tmpRepo, err := PlainOpen(tmpDir) 346 + if err != nil { 347 + return err 348 + } 349 + 350 + result := tmpRepo.applyPatch(patchData, patchFile, mo) 351 351 mergeCheckCache.Set(g, patchData, targetBranch, result) 352 352 return result 353 353 } 354 354 355 355 func (g *GitRepo) MergeWithOptions(patchData string, targetBranch string, opts MergeOptions) error { 356 - patchFile, err := g.createTempFileWithPatch(patchData) 356 + patchFile, err := createTempFileWithPatch(patchData) 357 357 if err != nil { 358 358 return &ErrMerge{ 359 359 Message: err.Error(), ··· 362 362 } 363 363 defer os.Remove(patchFile) 364 364 365 - tmpDir, err := g.cloneRepository(targetBranch) 365 + tmpDir, err := g.cloneTemp(targetBranch) 366 366 if err != nil { 367 367 return &ErrMerge{ 368 368 Message: err.Error(),
+706
knotserver/git/merge_test.go
··· 1 + package git 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + "strings" 7 + "testing" 8 + 9 + "github.com/go-git/go-git/v5" 10 + "github.com/go-git/go-git/v5/config" 11 + "github.com/go-git/go-git/v5/plumbing" 12 + "github.com/go-git/go-git/v5/plumbing/object" 13 + "github.com/stretchr/testify/assert" 14 + "github.com/stretchr/testify/require" 15 + ) 16 + 17 + type Helper struct { 18 + t *testing.T 19 + tempDir string 20 + repo *GitRepo 21 + } 22 + 23 + func helper(t *testing.T) *Helper { 24 + tempDir, err := os.MkdirTemp("", "git-merge-test-*") 25 + require.NoError(t, err) 26 + 27 + return &Helper{ 28 + t: t, 29 + tempDir: tempDir, 30 + } 31 + } 32 + 33 + func (h *Helper) cleanup() { 34 + if h.tempDir != "" { 35 + os.RemoveAll(h.tempDir) 36 + } 37 + } 38 + 39 + // initRepo initializes a git repository with an initial commit 40 + func (h *Helper) initRepo() *GitRepo { 41 + repoPath := filepath.Join(h.tempDir, "test-repo") 42 + 43 + // initialize repository 44 + r, err := git.PlainInit(repoPath, false) 45 + require.NoError(h.t, err) 46 + 47 + // configure git user 48 + cfg, err := r.Config() 49 + require.NoError(h.t, err) 50 + cfg.User.Name = "Test User" 51 + cfg.User.Email = "test@example.com" 52 + err = r.SetConfig(cfg) 53 + require.NoError(h.t, err) 54 + 55 + // create initial commit with a file 56 + w, err := r.Worktree() 57 + require.NoError(h.t, err) 58 + 59 + // create initial file 60 + initialFile := filepath.Join(repoPath, "README.md") 61 + err = os.WriteFile(initialFile, []byte("# Test Repository\n\nInitial content.\n"), 0644) 62 + require.NoError(h.t, err) 63 + 64 + _, err = w.Add("README.md") 65 + require.NoError(h.t, err) 66 + 67 + _, err = w.Commit("Initial commit", &git.CommitOptions{ 68 + Author: &object.Signature{ 69 + Name: "Test User", 70 + Email: "test@example.com", 71 + }, 72 + }) 73 + require.NoError(h.t, err) 74 + 75 + gitRepo, err := PlainOpen(repoPath) 76 + require.NoError(h.t, err) 77 + 78 + h.repo = gitRepo 79 + return gitRepo 80 + } 81 + 82 + // addFile creates a file in the repository 83 + func (h *Helper) addFile(filename, content string) { 84 + filePath := filepath.Join(h.repo.path, filename) 85 + dir := filepath.Dir(filePath) 86 + 87 + err := os.MkdirAll(dir, 0755) 88 + require.NoError(h.t, err) 89 + 90 + err = os.WriteFile(filePath, []byte(content), 0644) 91 + require.NoError(h.t, err) 92 + } 93 + 94 + // commitFile adds and commits a file 95 + func (h *Helper) commitFile(filename, content, message string) plumbing.Hash { 96 + h.addFile(filename, content) 97 + 98 + w, err := h.repo.r.Worktree() 99 + require.NoError(h.t, err) 100 + 101 + _, err = w.Add(filename) 102 + require.NoError(h.t, err) 103 + 104 + hash, err := w.Commit(message, &git.CommitOptions{ 105 + Author: &object.Signature{ 106 + Name: "Test User", 107 + Email: "test@example.com", 108 + }, 109 + }) 110 + require.NoError(h.t, err) 111 + 112 + return hash 113 + } 114 + 115 + // readFile reads a file from the repository 116 + func (h *Helper) readFile(filename string) string { 117 + content, err := os.ReadFile(filepath.Join(h.repo.path, filename)) 118 + require.NoError(h.t, err) 119 + return string(content) 120 + } 121 + 122 + // fileExists checks if a file exists in the repository 123 + func (h *Helper) fileExists(filename string) bool { 124 + _, err := os.Stat(filepath.Join(h.repo.path, filename)) 125 + return err == nil 126 + } 127 + 128 + func TestApplyPatch_Success(t *testing.T) { 129 + h := helper(t) 130 + defer h.cleanup() 131 + 132 + repo := h.initRepo() 133 + 134 + // modify README.md 135 + patch := `diff --git a/README.md b/README.md 136 + index 1234567..abcdefg 100644 137 + --- a/README.md 138 + +++ b/README.md 139 + @@ -1,3 +1,3 @@ 140 + # Test Repository 141 + 142 + -Initial content. 143 + +Modified content. 144 + ` 145 + 146 + patchFile, err := createTempFileWithPatch(patch) 147 + require.NoError(t, err) 148 + defer os.Remove(patchFile) 149 + 150 + opts := MergeOptions{ 151 + CommitMessage: "Apply test patch", 152 + CommitterName: "Test Committer", 153 + CommitterEmail: "committer@example.com", 154 + FormatPatch: false, 155 + } 156 + 157 + err = repo.applyPatch(patch, patchFile, opts) 158 + assert.NoError(t, err) 159 + 160 + // verify the file was modified 161 + content := h.readFile("README.md") 162 + assert.Contains(t, content, "Modified content.") 163 + } 164 + 165 + func TestApplyPatch_AddNewFile(t *testing.T) { 166 + h := helper(t) 167 + defer h.cleanup() 168 + 169 + repo := h.initRepo() 170 + 171 + // add a new file 172 + patch := `diff --git a/newfile.txt b/newfile.txt 173 + new file mode 100644 174 + index 0000000..ce01362 175 + --- /dev/null 176 + +++ b/newfile.txt 177 + @@ -0,0 +1 @@ 178 + +hello 179 + ` 180 + 181 + patchFile, err := createTempFileWithPatch(patch) 182 + require.NoError(t, err) 183 + defer os.Remove(patchFile) 184 + 185 + opts := MergeOptions{ 186 + CommitMessage: "Add new file", 187 + CommitterName: "Test Committer", 188 + CommitterEmail: "committer@example.com", 189 + FormatPatch: false, 190 + } 191 + 192 + err = repo.applyPatch(patch, patchFile, opts) 193 + assert.NoError(t, err) 194 + 195 + assert.True(t, h.fileExists("newfile.txt")) 196 + content := h.readFile("newfile.txt") 197 + assert.Equal(t, "hello\n", content) 198 + } 199 + 200 + func TestApplyPatch_DeleteFile(t *testing.T) { 201 + h := helper(t) 202 + defer h.cleanup() 203 + 204 + repo := h.initRepo() 205 + 206 + // add a file 207 + h.commitFile("deleteme.txt", "content to delete\n", "Add file to delete") 208 + 209 + // delete the file 210 + patch := `diff --git a/deleteme.txt b/deleteme.txt 211 + deleted file mode 100644 212 + index 1234567..0000000 213 + --- a/deleteme.txt 214 + +++ /dev/null 215 + @@ -1 +0,0 @@ 216 + -content to delete 217 + ` 218 + 219 + patchFile, err := createTempFileWithPatch(patch) 220 + require.NoError(t, err) 221 + defer os.Remove(patchFile) 222 + 223 + opts := MergeOptions{ 224 + CommitMessage: "Delete file", 225 + CommitterName: "Test Committer", 226 + CommitterEmail: "committer@example.com", 227 + FormatPatch: false, 228 + } 229 + 230 + err = repo.applyPatch(patch, patchFile, opts) 231 + assert.NoError(t, err) 232 + 233 + assert.False(t, h.fileExists("deleteme.txt")) 234 + } 235 + 236 + func TestApplyPatch_WithAuthor(t *testing.T) { 237 + h := helper(t) 238 + defer h.cleanup() 239 + 240 + repo := h.initRepo() 241 + 242 + patch := `diff --git a/README.md b/README.md 243 + index 1234567..abcdefg 100644 244 + --- a/README.md 245 + +++ b/README.md 246 + @@ -1,3 +1,4 @@ 247 + # Test Repository 248 + 249 + Initial content. 250 + +New line. 251 + ` 252 + 253 + patchFile, err := createTempFileWithPatch(patch) 254 + require.NoError(t, err) 255 + defer os.Remove(patchFile) 256 + 257 + opts := MergeOptions{ 258 + CommitMessage: "Patch with author", 259 + AuthorName: "Patch Author", 260 + AuthorEmail: "author@example.com", 261 + CommitterName: "Test Committer", 262 + CommitterEmail: "committer@example.com", 263 + FormatPatch: false, 264 + } 265 + 266 + err = repo.applyPatch(patch, patchFile, opts) 267 + assert.NoError(t, err) 268 + 269 + head, err := repo.r.Head() 270 + require.NoError(t, err) 271 + 272 + commit, err := repo.r.CommitObject(head.Hash()) 273 + require.NoError(t, err) 274 + 275 + assert.Equal(t, "Patch Author", commit.Author.Name) 276 + assert.Equal(t, "author@example.com", commit.Author.Email) 277 + } 278 + 279 + func TestApplyPatch_MissingFile(t *testing.T) { 280 + h := helper(t) 281 + defer h.cleanup() 282 + 283 + repo := h.initRepo() 284 + 285 + // patch that modifies a non-existent file 286 + patch := `diff --git a/nonexistent.txt b/nonexistent.txt 287 + index 1234567..abcdefg 100644 288 + --- a/nonexistent.txt 289 + +++ b/nonexistent.txt 290 + @@ -1 +1 @@ 291 + -old content 292 + +new content 293 + ` 294 + 295 + patchFile, err := createTempFileWithPatch(patch) 296 + require.NoError(t, err) 297 + defer os.Remove(patchFile) 298 + 299 + opts := MergeOptions{ 300 + CommitMessage: "Should fail", 301 + CommitterName: "Test Committer", 302 + CommitterEmail: "committer@example.com", 303 + FormatPatch: false, 304 + } 305 + 306 + err = repo.applyPatch(patch, patchFile, opts) 307 + assert.Error(t, err) 308 + assert.Contains(t, err.Error(), "patch application failed") 309 + } 310 + 311 + func TestApplyPatch_Conflict(t *testing.T) { 312 + h := helper(t) 313 + defer h.cleanup() 314 + 315 + repo := h.initRepo() 316 + 317 + // modify the file to create a conflict 318 + h.commitFile("README.md", "# Test Repository\n\nDifferent content.\n", "Modify README") 319 + 320 + // patch that expects different content 321 + patch := `diff --git a/README.md b/README.md 322 + index 1234567..abcdefg 100644 323 + --- a/README.md 324 + +++ b/README.md 325 + @@ -1,3 +1,3 @@ 326 + # Test Repository 327 + 328 + -Initial content. 329 + +Modified content. 330 + ` 331 + 332 + patchFile, err := createTempFileWithPatch(patch) 333 + require.NoError(t, err) 334 + defer os.Remove(patchFile) 335 + 336 + opts := MergeOptions{ 337 + CommitMessage: "Should conflict", 338 + CommitterName: "Test Committer", 339 + CommitterEmail: "committer@example.com", 340 + FormatPatch: false, 341 + } 342 + 343 + err = repo.applyPatch(patch, patchFile, opts) 344 + assert.Error(t, err) 345 + } 346 + 347 + func TestApplyPatch_MissingDirectory(t *testing.T) { 348 + h := helper(t) 349 + defer h.cleanup() 350 + 351 + repo := h.initRepo() 352 + 353 + // patch that adds a file in a non-existent directory 354 + patch := `diff --git a/subdir/newfile.txt b/subdir/newfile.txt 355 + new file mode 100644 356 + index 0000000..ce01362 357 + --- /dev/null 358 + +++ b/subdir/newfile.txt 359 + @@ -0,0 +1 @@ 360 + +content 361 + ` 362 + 363 + patchFile, err := createTempFileWithPatch(patch) 364 + require.NoError(t, err) 365 + defer os.Remove(patchFile) 366 + 367 + opts := MergeOptions{ 368 + CommitMessage: "Add file in subdir", 369 + CommitterName: "Test Committer", 370 + CommitterEmail: "committer@example.com", 371 + FormatPatch: false, 372 + } 373 + 374 + // git apply should create the directory automatically 375 + err = repo.applyPatch(patch, patchFile, opts) 376 + assert.NoError(t, err) 377 + 378 + // Verify the file and directory were created 379 + assert.True(t, h.fileExists("subdir/newfile.txt")) 380 + } 381 + 382 + func TestApplyMailbox_Single(t *testing.T) { 383 + h := helper(t) 384 + defer h.cleanup() 385 + 386 + repo := h.initRepo() 387 + 388 + // format-patch mailbox format 389 + patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 390 + From: Patch Author <author@example.com> 391 + Date: Mon, 1 Jan 2024 12:00:00 +0000 392 + Subject: [PATCH] Add new feature 393 + 394 + This is a test patch. 395 + --- 396 + newfile.txt | 1 + 397 + 1 file changed, 1 insertion(+) 398 + create mode 100644 newfile.txt 399 + 400 + diff --git a/newfile.txt b/newfile.txt 401 + new file mode 100644 402 + index 0000000..ce01362 403 + --- /dev/null 404 + +++ b/newfile.txt 405 + @@ -0,0 +1 @@ 406 + +hello 407 + -- 408 + 2.40.0 409 + ` 410 + 411 + err := repo.applyMailbox(patch) 412 + assert.NoError(t, err) 413 + 414 + assert.True(t, h.fileExists("newfile.txt")) 415 + content := h.readFile("newfile.txt") 416 + assert.Equal(t, "hello\n", content) 417 + } 418 + 419 + func TestApplyMailbox_Multiple(t *testing.T) { 420 + h := helper(t) 421 + defer h.cleanup() 422 + 423 + repo := h.initRepo() 424 + 425 + // multiple patches in mailbox format 426 + patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 427 + From: Patch Author <author@example.com> 428 + Date: Mon, 1 Jan 2024 12:00:00 +0000 429 + Subject: [PATCH 1/2] Add first file 430 + 431 + --- 432 + file1.txt | 1 + 433 + 1 file changed, 1 insertion(+) 434 + create mode 100644 file1.txt 435 + 436 + diff --git a/file1.txt b/file1.txt 437 + new file mode 100644 438 + index 0000000..ce01362 439 + --- /dev/null 440 + +++ b/file1.txt 441 + @@ -0,0 +1 @@ 442 + +first 443 + -- 444 + 2.40.0 445 + 446 + From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 447 + From: Patch Author <author@example.com> 448 + Date: Mon, 1 Jan 2024 12:01:00 +0000 449 + Subject: [PATCH 2/2] Add second file 450 + 451 + --- 452 + file2.txt | 1 + 453 + 1 file changed, 1 insertion(+) 454 + create mode 100644 file2.txt 455 + 456 + diff --git a/file2.txt b/file2.txt 457 + new file mode 100644 458 + index 0000000..ce01362 459 + --- /dev/null 460 + +++ b/file2.txt 461 + @@ -0,0 +1 @@ 462 + +second 463 + -- 464 + 2.40.0 465 + ` 466 + 467 + err := repo.applyMailbox(patch) 468 + assert.NoError(t, err) 469 + 470 + assert.True(t, h.fileExists("file1.txt")) 471 + assert.True(t, h.fileExists("file2.txt")) 472 + 473 + content1 := h.readFile("file1.txt") 474 + assert.Equal(t, "first\n", content1) 475 + 476 + content2 := h.readFile("file2.txt") 477 + assert.Equal(t, "second\n", content2) 478 + } 479 + 480 + func TestApplyMailbox_Conflict(t *testing.T) { 481 + h := helper(t) 482 + defer h.cleanup() 483 + 484 + repo := h.initRepo() 485 + 486 + h.commitFile("README.md", "# Test Repository\n\nConflicting content.\n", "Create conflict") 487 + 488 + patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 489 + From: Patch Author <author@example.com> 490 + Date: Mon, 1 Jan 2024 12:00:00 +0000 491 + Subject: [PATCH] Modify README 492 + 493 + --- 494 + README.md | 2 +- 495 + 1 file changed, 1 insertion(+), 1 deletion(-) 496 + 497 + diff --git a/README.md b/README.md 498 + index 1234567..abcdefg 100644 499 + --- a/README.md 500 + +++ b/README.md 501 + @@ -1,3 +1,3 @@ 502 + # Test Repository 503 + 504 + -Initial content. 505 + +Different content. 506 + -- 507 + 2.40.0 508 + ` 509 + 510 + err := repo.applyMailbox(patch) 511 + assert.Error(t, err) 512 + 513 + var mergeErr *ErrMerge 514 + assert.ErrorAs(t, err, &mergeErr) 515 + } 516 + 517 + func TestParseGitApplyErrors(t *testing.T) { 518 + tests := []struct { 519 + name string 520 + errorOutput string 521 + expectedCount int 522 + expectedReason string 523 + }{ 524 + { 525 + name: "file already exists", 526 + errorOutput: `error: path/to/file.txt: already exists in working directory`, 527 + expectedCount: 1, 528 + expectedReason: "file already exists", 529 + }, 530 + { 531 + name: "file does not exist", 532 + errorOutput: `error: path/to/file.txt: does not exist in working tree`, 533 + expectedCount: 1, 534 + expectedReason: "file does not exist", 535 + }, 536 + { 537 + name: "patch does not apply", 538 + errorOutput: `error: patch failed: file.txt:10 539 + error: file.txt: patch does not apply`, 540 + expectedCount: 1, 541 + expectedReason: "patch does not apply", 542 + }, 543 + { 544 + name: "multiple conflicts", 545 + errorOutput: `error: patch failed: file1.txt:5 546 + error: file1.txt:5: some error 547 + error: patch failed: file2.txt:10 548 + error: file2.txt:10: another error`, 549 + expectedCount: 2, 550 + }, 551 + } 552 + 553 + for _, tt := range tests { 554 + t.Run(tt.name, func(t *testing.T) { 555 + conflicts := parseGitApplyErrors(tt.errorOutput) 556 + assert.Len(t, conflicts, tt.expectedCount) 557 + 558 + if tt.expectedReason != "" && len(conflicts) > 0 { 559 + assert.Equal(t, tt.expectedReason, conflicts[0].Reason) 560 + } 561 + }) 562 + } 563 + } 564 + 565 + func TestErrMerge_Error(t *testing.T) { 566 + tests := []struct { 567 + name string 568 + err ErrMerge 569 + expectedMsg string 570 + }{ 571 + { 572 + name: "with conflicts", 573 + err: ErrMerge{ 574 + Message: "test merge failed", 575 + HasConflict: true, 576 + Conflicts: []ConflictInfo{ 577 + {Filename: "file1.txt", Reason: "conflict 1"}, 578 + {Filename: "file2.txt", Reason: "conflict 2"}, 579 + }, 580 + }, 581 + expectedMsg: "merge failed due to conflicts: test merge failed (2 conflicts)", 582 + }, 583 + { 584 + name: "with other error", 585 + err: ErrMerge{ 586 + Message: "command failed", 587 + OtherError: assert.AnError, 588 + }, 589 + expectedMsg: "merge failed: command failed:", 590 + }, 591 + { 592 + name: "message only", 593 + err: ErrMerge{ 594 + Message: "simple failure", 595 + }, 596 + expectedMsg: "merge failed: simple failure", 597 + }, 598 + } 599 + 600 + for _, tt := range tests { 601 + t.Run(tt.name, func(t *testing.T) { 602 + errMsg := tt.err.Error() 603 + assert.Contains(t, errMsg, tt.expectedMsg) 604 + }) 605 + } 606 + } 607 + 608 + func TestMergeWithOptions_Integration(t *testing.T) { 609 + h := helper(t) 610 + defer h.cleanup() 611 + 612 + // create a repository first with initial content 613 + workRepoPath := filepath.Join(h.tempDir, "work-repo") 614 + workRepo, err := git.PlainInit(workRepoPath, false) 615 + require.NoError(t, err) 616 + 617 + // configure git user 618 + cfg, err := workRepo.Config() 619 + require.NoError(t, err) 620 + cfg.User.Name = "Test User" 621 + cfg.User.Email = "test@example.com" 622 + err = workRepo.SetConfig(cfg) 623 + require.NoError(t, err) 624 + 625 + // Create initial commit 626 + w, err := workRepo.Worktree() 627 + require.NoError(t, err) 628 + 629 + err = os.WriteFile(filepath.Join(workRepoPath, "README.md"), []byte("# Initial\n"), 0644) 630 + require.NoError(t, err) 631 + 632 + _, err = w.Add("README.md") 633 + require.NoError(t, err) 634 + 635 + _, err = w.Commit("Initial commit", &git.CommitOptions{ 636 + Author: &object.Signature{ 637 + Name: "Test User", 638 + Email: "test@example.com", 639 + }, 640 + }) 641 + require.NoError(t, err) 642 + 643 + // create a bare repository (like production) 644 + bareRepoPath := filepath.Join(h.tempDir, "bare-repo") 645 + err = InitBare(bareRepoPath, "main") 646 + require.NoError(t, err) 647 + 648 + // add bare repo as remote and push to it 649 + _, err = workRepo.CreateRemote(&config.RemoteConfig{ 650 + Name: "origin", 651 + URLs: []string{"file://" + bareRepoPath}, 652 + }) 653 + require.NoError(t, err) 654 + 655 + err = workRepo.Push(&git.PushOptions{ 656 + RemoteName: "origin", 657 + RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/main"}, 658 + }) 659 + require.NoError(t, err) 660 + 661 + // now merge a patch into the bare repo 662 + gitRepo, err := PlainOpen(bareRepoPath) 663 + require.NoError(t, err) 664 + 665 + patch := `diff --git a/feature.txt b/feature.txt 666 + new file mode 100644 667 + index 0000000..5e1c309 668 + --- /dev/null 669 + +++ b/feature.txt 670 + @@ -0,0 +1 @@ 671 + +Hello World 672 + ` 673 + 674 + opts := MergeOptions{ 675 + CommitMessage: "Add feature", 676 + CommitterName: "Test Committer", 677 + CommitterEmail: "committer@example.com", 678 + FormatPatch: false, 679 + } 680 + 681 + err = gitRepo.MergeWithOptions(patch, "main", opts) 682 + assert.NoError(t, err) 683 + 684 + // Clone again and verify the changes were merged 685 + verifyRepoPath := filepath.Join(h.tempDir, "verify-repo") 686 + verifyRepo, err := git.PlainClone(verifyRepoPath, false, &git.CloneOptions{ 687 + URL: "file://" + bareRepoPath, 688 + }) 689 + require.NoError(t, err) 690 + 691 + // check that feature.txt exists 692 + featureFile := filepath.Join(verifyRepoPath, "feature.txt") 693 + assert.FileExists(t, featureFile) 694 + 695 + content, err := os.ReadFile(featureFile) 696 + require.NoError(t, err) 697 + assert.Equal(t, "Hello World\n", string(content)) 698 + 699 + // verify commit message 700 + head, err := verifyRepo.Head() 701 + require.NoError(t, err) 702 + 703 + commit, err := verifyRepo.CommitObject(head.Hash()) 704 + require.NoError(t, err) 705 + assert.Equal(t, "Add feature", strings.TrimSpace(commit.Message)) 706 + }
-25
knotserver/router.go
··· 5 5 "fmt" 6 6 "log/slog" 7 7 "net/http" 8 - "strings" 9 8 10 9 "github.com/go-chi/chi/v5" 11 10 "tangled.org/core/idresolver" ··· 80 79 }) 81 80 82 81 r.Route("/{did}", func(r chi.Router) { 83 - r.Use(h.resolveDidRedirect) 84 82 r.Route("/{name}", func(r chi.Router) { 85 83 // routes for git operations 86 84 r.Get("/info/refs", h.InfoRefs) ··· 116 114 } 117 115 118 116 return xrpc.Router() 119 - } 120 - 121 - func (h *Knot) resolveDidRedirect(next http.Handler) http.Handler { 122 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 123 - didOrHandle := chi.URLParam(r, "did") 124 - if strings.HasPrefix(didOrHandle, "did:") { 125 - next.ServeHTTP(w, r) 126 - return 127 - } 128 - 129 - trimmed := strings.TrimPrefix(didOrHandle, "@") 130 - id, err := h.resolver.ResolveIdent(r.Context(), trimmed) 131 - if err != nil { 132 - // invalid did or handle 133 - h.l.Error("failed to resolve did/handle", "handle", trimmed, "err", err) 134 - http.Error(w, fmt.Sprintf("failed to resolve did/handle: %s", trimmed), http.StatusInternalServerError) 135 - return 136 - } 137 - 138 - suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 139 - newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 140 - http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 141 - }) 142 117 } 143 118 144 119 func (h *Knot) configureOwner() error {
+7 -1
knotserver/xrpc/merge_check.go
··· 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 10 "tangled.org/core/api/tangled" 11 11 "tangled.org/core/knotserver/git" 12 + "tangled.org/core/patchutil" 12 13 xrpcerr "tangled.org/core/xrpc/errors" 13 14 ) 14 15 ··· 51 52 return 52 53 } 53 54 54 - err = gr.MergeCheck(data.Patch, data.Branch) 55 + mo := git.MergeOptions{} 56 + mo.CommitterName = x.Config.Git.UserName 57 + mo.CommitterEmail = x.Config.Git.UserEmail 58 + mo.FormatPatch = patchutil.IsFormatPatch(data.Patch) 59 + 60 + err = gr.MergeCheckWithOptions(data.Patch, data.Branch, mo) 55 61 56 62 response := tangled.RepoMergeCheck_Output{ 57 63 Is_conflicted: false,
+1 -2
lexicons/actor/profile.json
··· 45 45 "open-pull-request-count", 46 46 "open-issue-count", 47 47 "closed-issue-count", 48 - "repository-count", 49 - "star-count" 48 + "repository-count" 50 49 ] 51 50 } 52 51 },