Skip to content

Commit f8d395c

Browse files
committed
Merge branch 'telackey/sort' of github.com:telackey/gitea into telackey/sort
2 parents fdb2dd0 + 399be95 commit f8d395c

File tree

10 files changed

+148
-59
lines changed

10 files changed

+148
-59
lines changed

modules/templates/util_avatar.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ func AvatarHTML(src string, size int, class, name string) template.HTML {
3434
name = "avatar"
3535
}
3636

37-
return template.HTML(`<img loading="lazy" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
37+
// use empty alt, otherwise if the image fails to load, the width will follow the "alt" text's width
38+
return template.HTML(`<img loading="lazy" alt="" class="` + class + `" src="` + src + `" title="` + html.EscapeString(name) + `" width="` + sizeStr + `" height="` + sizeStr + `"/>`)
3839
}
3940

4041
// Avatar renders user avatars. args: user, size (int), class (string)

services/mirror/mirror_pull.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,24 @@ func pruneBrokenReferences(ctx context.Context,
235235
return pruneErr
236236
}
237237

238+
// checkRecoverableSyncError takes an error message from a git fetch command and returns false if it should be a fatal/blocking error
239+
func checkRecoverableSyncError(stderrMessage string) bool {
240+
switch {
241+
case strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken"):
242+
return true
243+
case strings.Contains(stderrMessage, "remote error") && strings.Contains(stderrMessage, "not our ref"):
244+
return true
245+
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "but expected"):
246+
return true
247+
case strings.Contains(stderrMessage, "cannot lock ref") && strings.Contains(stderrMessage, "unable to resolve reference"):
248+
return true
249+
case strings.Contains(stderrMessage, "Unable to create") && strings.Contains(stderrMessage, ".lock"):
250+
return true
251+
default:
252+
return false
253+
}
254+
}
255+
238256
// runSync returns true if sync finished without error.
239257
func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bool) {
240258
repoPath := m.Repo.RepoPath()
@@ -275,7 +293,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
275293
stdoutMessage := util.SanitizeCredentialURLs(stdout)
276294

277295
// Now check if the error is a resolve reference due to broken reference
278-
if strings.Contains(stderr, "unable to resolve reference") && strings.Contains(stderr, "reference broken") {
296+
if checkRecoverableSyncError(stderr) {
279297
log.Warn("SyncMirrors [repo: %-v]: failed to update mirror repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
280298
err = nil
281299

@@ -324,6 +342,15 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
324342
return nil, false
325343
}
326344

345+
if m.LFS && setting.LFS.StartServer {
346+
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
347+
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
348+
lfsClient := lfs.NewClient(endpoint, nil)
349+
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
350+
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
351+
}
352+
}
353+
327354
log.Trace("SyncMirrors [repo: %-v]: syncing branches...", m.Repo)
328355
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, m.Repo, gitRepo, 0); err != nil {
329356
log.Error("SyncMirrors [repo: %-v]: failed to synchronize branches: %v", m.Repo, err)
@@ -333,15 +360,6 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
333360
if err = repo_module.SyncReleasesWithTags(ctx, m.Repo, gitRepo); err != nil {
334361
log.Error("SyncMirrors [repo: %-v]: failed to synchronize tags to releases: %v", m.Repo, err)
335362
}
336-
337-
if m.LFS && setting.LFS.StartServer {
338-
log.Trace("SyncMirrors [repo: %-v]: syncing LFS objects...", m.Repo)
339-
endpoint := lfs.DetermineEndpoint(remoteURL.String(), m.LFSEndpoint)
340-
lfsClient := lfs.NewClient(endpoint, nil)
341-
if err = repo_module.StoreMissingLfsObjectsInRepository(ctx, m.Repo, gitRepo, lfsClient); err != nil {
342-
log.Error("SyncMirrors [repo: %-v]: failed to synchronize LFS objects for repository: %v", m.Repo, err)
343-
}
344-
}
345363
gitRepo.Close()
346364

347365
log.Trace("SyncMirrors [repo: %-v]: updating size of repository", m.Repo)
@@ -368,7 +386,7 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo
368386
stdoutMessage := util.SanitizeCredentialURLs(stdout)
369387

370388
// Now check if the error is a resolve reference due to broken reference
371-
if strings.Contains(stderrMessage, "unable to resolve reference") && strings.Contains(stderrMessage, "reference broken") {
389+
if checkRecoverableSyncError(stderrMessage) {
372390
log.Warn("SyncMirrors [repo: %-v Wiki]: failed to update mirror wiki repository due to broken references:\nStdout: %s\nStderr: %s\nErr: %v\nAttempting Prune", m.Repo, stdoutMessage, stderrMessage, err)
373391
err = nil
374392

services/mirror/mirror_test.go renamed to services/mirror/mirror_pull_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,31 @@ func Test_parseRemoteUpdateOutput(t *testing.T) {
6464
assert.EqualValues(t, "1c97ebc746", results[9].oldCommitID)
6565
assert.EqualValues(t, "976d27d52f", results[9].newCommitID)
6666
}
67+
68+
func Test_checkRecoverableSyncError(t *testing.T) {
69+
cases := []struct {
70+
recoverable bool
71+
message string
72+
}{
73+
// A race condition in http git-fetch where certain refs were listed on the remote and are no longer there, would exit status 128
74+
{true, "fatal: remote error: upload-pack: not our ref 988881adc9fc3655077dc2d4d757d480b5ea0e11"},
75+
// A race condition where a local gc/prune removes a named ref during a git-fetch would exit status 1
76+
{true, "cannot lock ref 'refs/pull/123456/merge': unable to resolve reference 'refs/pull/134153/merge'"},
77+
// A race condition in http git-fetch where named refs were listed on the remote and are no longer there
78+
{true, "error: cannot lock ref 'refs/remotes/origin/foo': unable to resolve reference 'refs/remotes/origin/foo': reference broken"},
79+
// A race condition in http git-fetch where named refs were force-pushed during the update, would exit status 128
80+
{true, "error: cannot lock ref 'refs/pull/123456/merge': is at 988881adc9fc3655077dc2d4d757d480b5ea0e11 but expected 7f894307ffc9553edbd0b671cab829786866f7b2"},
81+
// A race condition with other local git operations, such as git-maintenance, would exit status 128 (well, "Unable" the "U" is uppercase)
82+
{true, "fatal: Unable to create '/data/gitea-repositories/foo-org/bar-repo.git/./objects/info/commit-graphs/commit-graph-chain.lock': File exists."},
83+
// Missing or unauthorized credentials, would exit status 128
84+
{false, "fatal: Authentication failed for 'https://example.com/foo-does-not-exist/bar.git/'"},
85+
// A non-existent remote repository, would exit status 128
86+
{false, "fatal: Could not read from remote repository."},
87+
// A non-functioning proxy, would exit status 128
88+
{false, "fatal: unable to access 'https://example.com/foo-does-not-exist/bar.git/': Failed to connect to configured-https-proxy port 1080 after 0 ms: Couldn't connect to server"},
89+
}
90+
91+
for _, c := range cases {
92+
assert.Equal(t, c.recoverable, checkRecoverableSyncError(c.message), "test case: %s", c.message)
93+
}
94+
}

templates/repo/icon.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{{$avatarLink := (.RelAvatarLink ctx)}}
22
{{if $avatarLink}}
3-
<img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
3+
<img class="ui avatar tw-align-middle" src="{{$avatarLink}}" width="24" height="24" alt="" aria-hidden="true">
44
{{else if $.IsMirror}}
55
{{svg "octicon-mirror" 24}}
66
{{else if $.IsFork}}

web_src/fomantic/build/components/dropdown.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,7 @@ $.fn.dropdown = function(parameters) {
752752
if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
753753
module.show();
754754
}
755+
settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
755756
}
756757
;
757758
if(settings.useLabels && module.has.maxSelections()) {
@@ -3992,6 +3993,8 @@ $.fn.dropdown.settings = {
39923993
onShow : function(){},
39933994
onHide : function(){},
39943995

3996+
onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items
3997+
39953998
/* Component */
39963999
name : 'Dropdown',
39974000
namespace : 'dropdown',

web_src/js/modules/fomantic/dropdown.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,27 @@ test('hideScopedEmptyDividers-simple', () => {
2323
`);
2424
});
2525

26-
test('hideScopedEmptyDividers-hidden1', () => {
26+
test('hideScopedEmptyDividers-items-all-filtered', () => {
27+
const container = createElementFromHTML(`<div>
28+
<div class="any"></div>
29+
<div class="divider"></div>
30+
<div class="item filtered">a</div>
31+
<div class="item filtered">b</div>
32+
<div class="divider"></div>
33+
<div class="any"></div>
34+
</div>`);
35+
hideScopedEmptyDividers(container);
36+
expect(container.innerHTML).toEqual(`
37+
<div class="any"></div>
38+
<div class="divider hidden transition"></div>
39+
<div class="item filtered">a</div>
40+
<div class="item filtered">b</div>
41+
<div class="divider"></div>
42+
<div class="any"></div>
43+
`);
44+
});
45+
46+
test('hideScopedEmptyDividers-hide-last', () => {
2747
const container = createElementFromHTML(`<div>
2848
<div class="item">a</div>
2949
<div class="divider" data-scope="b"></div>
@@ -37,7 +57,7 @@ test('hideScopedEmptyDividers-hidden1', () => {
3757
`);
3858
});
3959

40-
test('hideScopedEmptyDividers-hidden2', () => {
60+
test('hideScopedEmptyDividers-scoped-items', () => {
4161
const container = createElementFromHTML(`<div>
4262
<div class="item" data-scope="">a</div>
4363
<div class="divider" data-scope="b"></div>

web_src/js/modules/fomantic/dropdown.ts

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,36 @@ const fomanticDropdownFn = $.fn.dropdown;
99
// use our own `$().dropdown` function to patch Fomantic's dropdown module
1010
export function initAriaDropdownPatch() {
1111
if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
12+
$.fn.dropdown.settings.onAfterFiltered = onAfterFiltered;
1213
$.fn.dropdown = ariaDropdownFn;
1314
$.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem;
1415
(ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
1516
}
1617

1718
// the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and:
18-
// * it does the one-time attaching on the first call
19-
// * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes
19+
// * it does the one-time element event attaching on the first call
20+
// * it delegates the module internal functions like `onLabelCreate` to the patched functions to add more features.
2021
function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) {
2122
const ret = fomanticDropdownFn.apply(this, args);
2223

23-
// if the `$().dropdown()` call is without arguments, or it has non-string (object) argument,
24-
// it means that this call will reset the dropdown internal settings, then we need to re-delegate the callbacks.
25-
const needDelegate = (!args.length || typeof args[0] !== 'string');
26-
for (const el of this) {
24+
for (let el of this) {
25+
// dropdown will replace '<select class="ui dropdown"/>' to '<div class="ui dropdown"><select (hidden)></select><div class="menu">...</div></div>'
26+
// so we need to correctly find the closest '.ui.dropdown' element, it is the real fomantic dropdown module.
27+
el = el.closest('.ui.dropdown');
2728
if (!el[ariaPatchKey]) {
28-
attachInit(el);
29+
// the elements don't belong to the dropdown "module" and won't be reset
30+
// so we only need to initialize them once.
31+
attachInitElements(el);
2932
}
30-
if (needDelegate) {
31-
delegateOne($(el));
33+
34+
// if the `$().dropdown()` is called without arguments, or it has non-string (object) argument,
35+
// it means that such call will reset the dropdown "module" including internal settings,
36+
// then we need to re-delegate the callbacks.
37+
const $dropdown = $(el);
38+
const dropdownModule = $dropdown.data('module-dropdown');
39+
if (!dropdownModule.giteaDelegated) {
40+
dropdownModule.giteaDelegated = true;
41+
delegateDropdownModule($dropdown);
3242
}
3343
}
3444
return ret;
@@ -61,37 +71,17 @@ function updateSelectionLabel(label: HTMLElement) {
6171
}
6272
}
6373

64-
function processMenuItems($dropdown: any, dropdownCall: any) {
65-
const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty';
74+
function onAfterFiltered(this: any) {
75+
const $dropdown = $(this);
76+
const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty';
6677
const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu');
6778
if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu);
6879
}
6980

7081
// delegate the dropdown's template functions and callback functions to add aria attributes.
71-
function delegateOne($dropdown: any) {
82+
function delegateDropdownModule($dropdown: any) {
7283
const dropdownCall = fomanticDropdownFn.bind($dropdown);
7384

74-
// If there is a "search input" in the "menu", Fomantic will only "focus the input" but not "toggle the menu" when the "dropdown icon" is clicked.
75-
// Actually, Fomantic UI doesn't support such layout/usage. It needs to patch the "focusSearch" / "blurSearch" functions to make sure it toggles the menu.
76-
const oldFocusSearch = dropdownCall('internal', 'focusSearch');
77-
const oldBlurSearch = dropdownCall('internal', 'blurSearch');
78-
// * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu
79-
dropdownCall('internal', 'focusSearch', function (this: any) { dropdownCall('show'); oldFocusSearch.call(this) });
80-
// * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu
81-
dropdownCall('internal', 'blurSearch', function (this: any) { oldBlurSearch.call(this); dropdownCall('hide') });
82-
83-
const oldFilterItems = dropdownCall('internal', 'filterItems');
84-
dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) {
85-
oldFilterItems.call(this, ...args);
86-
processMenuItems($dropdown, dropdownCall);
87-
});
88-
89-
const oldShow = dropdownCall('internal', 'show');
90-
dropdownCall('internal', 'show', function (this: any, ...args: any[]) {
91-
oldShow.call(this, ...args);
92-
processMenuItems($dropdown, dropdownCall);
93-
});
94-
9585
// the "template" functions are used for dynamic creation (eg: AJAX)
9686
const dropdownTemplates = {...dropdownCall('setting', 'templates'), t: performance.now()};
9787
const dropdownTemplatesMenuOld = dropdownTemplates.menu;
@@ -163,9 +153,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men
163153
}
164154
}
165155

166-
function attachInit(dropdown: HTMLElement) {
156+
function attachInitElements(dropdown: HTMLElement) {
167157
(dropdown as any)[ariaPatchKey] = {};
168-
if (dropdown.classList.contains('custom')) return;
169158

170159
// Dropdown has 2 different focusing behaviors
171160
// * with search input: the input is focused, and it works with aria-activedescendant pointing another sibling element.
@@ -305,9 +294,11 @@ export function hideScopedEmptyDividers(container: Element) {
305294
const visibleItems: Element[] = [];
306295
const curScopeVisibleItems: Element[] = [];
307296
let curScope: string = '', lastVisibleScope: string = '';
308-
const isScopedDivider = (item: Element) => item.matches('.divider') && item.hasAttribute('data-scope');
297+
const isDivider = (item: Element) => item.classList.contains('divider');
298+
const isScopedDivider = (item: Element) => isDivider(item) && item.hasAttribute('data-scope');
309299
const hideDivider = (item: Element) => item.classList.add('hidden', 'transition'); // dropdown has its own classes to hide items
310-
300+
const showDivider = (item: Element) => item.classList.remove('hidden', 'transition');
301+
const isHidden = (item: Element) => item.classList.contains('hidden') || item.classList.contains('filtered') || item.classList.contains('tw-hidden');
311302
const handleScopeSwitch = (itemScope: string) => {
312303
if (curScopeVisibleItems.length === 1 && isScopedDivider(curScopeVisibleItems[0])) {
313304
hideDivider(curScopeVisibleItems[0]);
@@ -323,34 +314,37 @@ export function hideScopedEmptyDividers(container: Element) {
323314
curScopeVisibleItems.length = 0;
324315
};
325316

317+
// reset hidden dividers
318+
queryElems(container, '.divider', showDivider);
319+
326320
// hide the scope dividers if the scope items are empty
327321
for (const item of container.children) {
328322
const itemScope = item.getAttribute('data-scope') || '';
329323
if (itemScope !== curScope) {
330324
handleScopeSwitch(itemScope);
331325
}
332-
if (!item.classList.contains('filtered') && !item.classList.contains('tw-hidden')) {
326+
if (!isHidden(item)) {
333327
curScopeVisibleItems.push(item as HTMLElement);
334328
}
335329
}
336330
handleScopeSwitch('');
337331

338332
// hide all leading and trailing dividers
339333
while (visibleItems.length) {
340-
if (!visibleItems[0].matches('.divider')) break;
334+
if (!isDivider(visibleItems[0])) break;
341335
hideDivider(visibleItems[0]);
342336
visibleItems.shift();
343337
}
344338
while (visibleItems.length) {
345-
if (!visibleItems[visibleItems.length - 1].matches('.divider')) break;
339+
if (!isDivider(visibleItems[visibleItems.length - 1])) break;
346340
hideDivider(visibleItems[visibleItems.length - 1]);
347341
visibleItems.pop();
348342
}
349343
// hide all duplicate dividers, hide current divider if next sibling is still divider
350344
// no need to update "visibleItems" array since this is the last loop
351-
for (const item of visibleItems) {
352-
if (!item.matches('.divider')) continue;
353-
if (item.nextElementSibling?.matches('.divider')) hideDivider(item);
345+
for (let i = 0; i < visibleItems.length - 1; i++) {
346+
if (!visibleItems[i].matches('.divider')) continue;
347+
if (visibleItems[i + 1].matches('.divider')) hideDivider(visibleItems[i]);
354348
}
355349
}
356350

web_src/js/modules/observer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ function callGlobalInitFunc(el: HTMLElement) {
4646
const func = globalInitFuncs[initFunc];
4747
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
4848

49+
// when an element node is removed and added again, it should not be re-initialized again.
4950
type GiteaGlobalInitElement = Partial<HTMLElement> & {_giteaGlobalInited: boolean};
50-
if ((el as GiteaGlobalInitElement)._giteaGlobalInited) throw new Error(`Global init function "${initFunc}" already executed`);
51+
if ((el as GiteaGlobalInitElement)._giteaGlobalInited) return;
5152
(el as GiteaGlobalInitElement)._giteaGlobalInited = true;
53+
5254
func(el);
5355
}
5456

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {weakRefClass} from './polyfills.ts';
2+
3+
test('polyfillWeakRef', () => {
4+
const WeakRef = weakRefClass();
5+
const r = new WeakRef(123);
6+
expect(r.deref()).toEqual(123);
7+
});

web_src/js/webcomponents/polyfills.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,19 @@ try {
1616
return intlNumberFormat(locales, options);
1717
};
1818
}
19+
20+
export function weakRefClass() {
21+
const weakMap = new WeakMap();
22+
return class {
23+
constructor(target: any) {
24+
weakMap.set(this, target);
25+
}
26+
deref() {
27+
return weakMap.get(this);
28+
}
29+
};
30+
}
31+
32+
if (!window.WeakRef) {
33+
window.WeakRef = weakRefClass() as any;
34+
}

0 commit comments

Comments
 (0)