Compare commits

..

No commits in common. "37191dcfbdbd007266a4d15a3c85cdf94cec1a7a" and "5849d4fde347cd1e47f2243b3239724c73b1261d" have entirely different histories.

15 changed files with 83 additions and 206 deletions

View File

@ -811,7 +811,7 @@ rules:
wc/no-constructor-params: [2] wc/no-constructor-params: [2]
wc/no-constructor: [2] wc/no-constructor: [2]
wc/no-customized-built-in-elements: [2] wc/no-customized-built-in-elements: [2]
wc/no-exports-with-element: [0] wc/no-exports-with-element: [2]
wc/no-invalid-element-name: [2] wc/no-invalid-element-name: [2]
wc/no-invalid-extends: [2] wc/no-invalid-extends: [2]
wc/no-method-prefixed-with-on: [2] wc/no-method-prefixed-with-on: [2]

View File

@ -40,19 +40,8 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
return req return req
} }
type MockContextOption struct {
Render context.Render
}
// MockContext mock context for unit tests // MockContext mock context for unit tests
func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*context.Context, *httptest.ResponseRecorder) { func MockContext(t *testing.T, reqPath string) (*context.Context, *httptest.ResponseRecorder) {
var opt MockContextOption
if len(opts) > 0 {
opt = opts[0]
}
if opt.Render == nil {
opt.Render = &MockRender{}
}
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req := mockRequest(t, reqPath) req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base, baseCleanUp := context.NewBaseContext(resp, req)
@ -60,7 +49,7 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := context.NewWebContext(base, opt.Render, nil) ctx := context.NewWebContext(base, &MockRender{}, nil)
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)

View File

@ -180,17 +180,11 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st
} }
if len(reqs) > 0 { if len(reqs) > 0 {
esBatchSize := 50 _, err := b.inner.Client.Bulk().
Index(b.inner.VersionedIndexName()).
for i := 0; i < len(reqs); i += esBatchSize { Add(reqs...).
_, err := b.inner.Client.Bulk(). Do(ctx)
Index(b.inner.VersionedIndexName()). return err
Add(reqs[i:min(i+esBatchSize, len(reqs))]...).
Do(ctx)
if err != nil {
return err
}
}
} }
return nil return nil
} }

View File

@ -133,18 +133,18 @@ type Writer struct {
Ctx *markup.RenderContext Ctx *markup.RenderContext
} }
func (r *Writer) resolveLink(kind, link string) string { const mailto = "mailto:"
link = strings.TrimPrefix(link, "file:")
if !strings.HasPrefix(link, "#") && // not a URL fragment func (r *Writer) resolveLink(l org.RegularLink) string {
!markup.IsLinkStr(link) && // not an absolute URL link := html.EscapeString(l.URL)
!strings.HasPrefix(link, "mailto:") { if l.Protocol == "file" {
if kind == "regular" { link = link[len("file:"):]
// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]" }
// so we need to try to guess the link kind again here if len(link) > 0 && !markup.IsLinkStr(link) &&
kind = org.RegularLink{URL: link}.Kind() link[0] != '#' && !strings.HasPrefix(link, mailto) {
}
base := r.Ctx.Links.Base base := r.Ctx.Links.Base
if kind == "image" || kind == "video" { switch l.Kind() {
case "image", "video":
base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki) base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
} }
link = util.URLJoin(base, link) link = util.URLJoin(base, link)
@ -154,29 +154,29 @@ func (r *Writer) resolveLink(kind, link string) string {
// WriteRegularLink renders images, links or videos // WriteRegularLink renders images, links or videos
func (r *Writer) WriteRegularLink(l org.RegularLink) { func (r *Writer) WriteRegularLink(l org.RegularLink) {
link := r.resolveLink(l.Kind(), l.URL) link := r.resolveLink(l)
// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427 // Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
switch l.Kind() { switch l.Kind() {
case "image": case "image":
if l.Description == nil { if l.Description == nil {
_, _ = fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link) fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
} else { } else {
imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) imageSrc := r.resolveLink(l.Description[0].(org.RegularLink))
_, _ = fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc) fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
} }
case "video": case "video":
if l.Description == nil { if l.Description == nil {
_, _ = fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link) fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
} else { } else {
videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...)) videoSrc := r.resolveLink(l.Description[0].(org.RegularLink))
_, _ = fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
} }
default: default:
description := link description := link
if l.Description != nil { if l.Description != nil {
description = r.WriteNodesAsString(l.Description...) description = r.WriteNodesAsString(l.Description...)
} }
_, _ = fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description) fmt.Fprintf(r, `<a href="%s">%s</a>`, link, description)
} }
} }

View File

@ -10,21 +10,26 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const AppURL = "http://localhost:3000/" const (
AppURL = "http://localhost:3000/"
Repo = "gogits/gogs"
AppSubURL = AppURL + Repo + "/"
)
func TestRender_StandardLinks(t *testing.T) { func TestRender_StandardLinks(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
Base: "/relative-path", Base: setting.AppSubURL,
BranchPath: "branch/main",
}, },
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
@ -33,30 +38,32 @@ func TestRender_StandardLinks(t *testing.T) {
test("[[https://google.com/]]", test("[[https://google.com/]]",
`<p><a href="https://google.com/">https://google.com/</a></p>`) `<p><a href="https://google.com/">https://google.com/</a></p>`)
test("[[WikiPage][The WikiPage Desc]]",
`<p><a href="/relative-path/WikiPage">The WikiPage Desc</a></p>`) lnk := util.URLJoin(AppSubURL, "WikiPage")
test("[[ImageLink.svg][The Image Desc]]", test("[[WikiPage][WikiPage]]",
`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`) `<p><a href="`+lnk+`">WikiPage</a></p>`)
} }
func TestRender_Media(t *testing.T) { func TestRender_Media(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{
Ctx: git.DefaultContext, Ctx: git.DefaultContext,
Links: markup.Links{ Links: markup.Links{
Base: "./relative-path", Base: setting.AppSubURL,
}, },
}, input) }, input)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
} }
test("[[file:../../.images/src/02/train.jpg]]", url := "../../.images/src/02/train.jpg"
`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg" /></p>`) result := util.URLJoin(AppSubURL, url)
test("[[file:train.jpg]]",
`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg" /></p>`) test("[[file:"+url+"]]",
`<p><img src="`+result+`" alt="`+result+`" /></p>`)
// With description. // With description.
test("[[https://example.com][https://example.com/example.svg]]", test("[[https://example.com][https://example.com/example.svg]]",
@ -73,20 +80,11 @@ func TestRender_Media(t *testing.T) {
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`) `<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
test("[[https://example.com/example.mp4]]", test("[[https://example.com/example.mp4]]",
`<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`) `<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
// test [[LINK][DESCRIPTION]] syntax with "file:" prefix
test(`[[https://example.com/][file:https://example.com/foo%20bar.svg]]`,
`<p><a href="https://example.com/"><img src="https://example.com/foo%20bar.svg" alt="https://example.com/foo%20bar.svg" /></a></p>`)
test(`[[file:https://example.com/foo%20bar.svg][Goto Image]]`,
`<p><a href="https://example.com/foo%20bar.svg">Goto Image</a></p>`)
test(`[[file:https://example.com/link][https://example.com/image.jpg]]`,
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
test(`[[file:https://example.com/link][file:https://example.com/image.jpg]]`,
`<p><a href="https://example.com/link"><img src="https://example.com/image.jpg" alt="https://example.com/image.jpg" /></a></p>`)
} }
func TestRender_Source(t *testing.T) { func TestRender_Source(t *testing.T) {
setting.AppURL = AppURL setting.AppURL = AppURL
setting.AppSubURL = AppSubURL
test := func(input, expected string) { test := func(input, expected string) {
buffer, err := RenderString(&markup.RenderContext{ buffer, err := RenderString(&markup.RenderContext{

View File

@ -98,15 +98,23 @@ func SetWhitespaceBehavior(ctx *context.Context) {
// SetShowOutdatedComments set the show outdated comments option as context variable // SetShowOutdatedComments set the show outdated comments option as context variable
func SetShowOutdatedComments(ctx *context.Context) { func SetShowOutdatedComments(ctx *context.Context) {
showOutdatedCommentsValue := ctx.FormString("show-outdated") showOutdatedCommentsValue := ctx.FormString("show-outdated")
// var showOutdatedCommentsValue string
if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" { if showOutdatedCommentsValue != "true" && showOutdatedCommentsValue != "false" {
// invalid or no value for this form string -> use default or stored user setting // invalid or no value for this form string -> use default or stored user setting
showOutdatedCommentsValue = "true"
if ctx.IsSigned { if ctx.IsSigned {
showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) showOutdatedCommentsValue, _ = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, "false")
} else {
// not logged in user -> use the default value
showOutdatedCommentsValue = "false"
} }
} else if ctx.IsSigned { } else {
// valid value -> update user setting if user is logged in // valid value -> update user setting if user is logged in
_ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue) if ctx.IsSigned {
_ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyShowOutdatedComments, showOutdatedCommentsValue)
}
} }
ctx.Data["ShowOutdatedComments"], _ = strconv.ParseBool(showOutdatedCommentsValue)
showOutdatedComments, _ := strconv.ParseBool(showOutdatedCommentsValue)
ctx.Data["ShowOutdatedComments"] = showOutdatedComments
} }

View File

@ -22,7 +22,6 @@ import (
const ( const (
tplDiffConversation base.TplName = "repo/diff/conversation" tplDiffConversation base.TplName = "repo/diff/conversation"
tplConversationOutdated base.TplName = "repo/diff/conversation_outdated"
tplTimelineConversation base.TplName = "repo/issue/view_content/conversation" tplTimelineConversation base.TplName = "repo/issue/view_content/conversation"
tplNewComment base.TplName = "repo/diff/new_comment" tplNewComment base.TplName = "repo/diff/new_comment"
) )
@ -162,8 +161,8 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return return
} }
if len(comments) == 0 { if len(comments) == 0 {
// if the comments are empty (deleted, outdated, etc), it's better to tell the users that it is outdated // if the comments are empty (deleted, outdated, etc), it doesn't need to render anything, just return an empty body to replace "conversation-holder" on the page
ctx.HTML(http.StatusOK, tplConversationOutdated) ctx.Resp.WriteHeader(http.StatusOK)
return return
} }

View File

@ -1,76 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http/httptest"
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/contexttest"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/pull"
"github.com/stretchr/testify/assert"
)
func TestRenderConversation(t *testing.T) {
unittest.PrepareTestEnv(t)
pr, _ := issues_model.GetPullRequestByID(db.DefaultContext, 2)
_ = pr.LoadIssue(db.DefaultContext)
_ = pr.Issue.LoadPoster(db.DefaultContext)
_ = pr.Issue.LoadRepo(db.DefaultContext)
run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) {
t.Run(name, func(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
contexttest.LoadUser(t, ctx, pr.Issue.PosterID)
contexttest.LoadRepo(t, ctx, pr.BaseRepoID)
contexttest.LoadGitRepo(t, ctx)
defer ctx.Repo.GitRepo.Close()
cb(t, ctx, resp)
})
}
var preparedComment *issues_model.Comment
run("prepare", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
comment, err := pull.CreateCodeComment(ctx, pr.Issue.Poster, ctx.Repo.GitRepo, pr.Issue, 1, "content", "", false, 0, pr.HeadCommitID)
if !assert.NoError(t, err) {
return
}
comment.Invalidated = true
err = issues_model.UpdateCommentInvalidate(ctx, comment)
if !assert.NoError(t, err) {
return
}
preparedComment = comment
})
if !assert.NotNil(t, preparedComment) {
return
}
run("diff with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = true
renderConversation(ctx, preparedComment, "diff")
assert.Contains(t, resp.Body.String(), `<div class="content comment-container"`)
})
run("diff without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = false
renderConversation(ctx, preparedComment, "diff")
assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
})
run("timeline with outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = true
renderConversation(ctx, preparedComment, "timeline")
assert.Contains(t, resp.Body.String(), `<div id="code-comments-`)
})
run("timeline without outdated", func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder) {
ctx.Data["ShowOutdatedComments"] = false
renderConversation(ctx, preparedComment, "timeline")
assert.Contains(t, resp.Body.String(), `conversation-not-existing`)
})
}

View File

@ -74,7 +74,6 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
"Release": rel, "Release": rel,
"Subject": subject, "Subject": subject,
"Language": locale.Language(), "Language": locale.Language(),
"Link": rel.HTMLURL(),
} }
var mailBody bytes.Buffer var mailBody bytes.Buffer

View File

@ -1,3 +0,0 @@
<div class="ui segment conversation-holder conversation-not-existing">
{{ctx.Locale.Tr "repo.issues.review.outdated_description"}}
</div>

View File

@ -1,6 +1,5 @@
import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
// for performance considerations, it only uses performant syntax // for performance considerations, it only uses performant syntax
function attachDirAuto(el) { function attachDirAuto(el) {
if (el.type !== 'hidden' && if (el.type !== 'hidden' &&
el.type !== 'checkbox' && el.type !== 'checkbox' &&
@ -19,7 +18,7 @@ export function initDirAuto() {
const len = mutation.addedNodes.length; const len = mutation.addedNodes.length;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const addedNode = mutation.addedNodes[i]; const addedNode = mutation.addedNodes[i];
if (!isDocumentFragmentOrElementNode(addedNode)) continue; if (addedNode.nodeType !== Node.ELEMENT_NODE && addedNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) continue;
if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode); if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode);
const children = addedNode.querySelectorAll('input, textarea'); const children = addedNode.querySelectorAll('input, textarea');
const len = children.length; const len = children.length;

View File

@ -1,5 +1,4 @@
import tippy, {followCursor} from 'tippy.js'; import tippy, {followCursor} from 'tippy.js';
import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
const visibleInstances = new Set(); const visibleInstances = new Set();
@ -137,6 +136,8 @@ function attachChildrenLazyTooltip(target) {
} }
} }
const elementNodeTypes = new Set([Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE]);
export function initGlobalTooltips() { export function initGlobalTooltips() {
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
const observerConnect = (observer) => observer.observe(document, { const observerConnect = (observer) => observer.observe(document, {
@ -151,10 +152,11 @@ export function initGlobalTooltips() {
if (mutation.type === 'childList') { if (mutation.type === 'childList') {
// mainly for Vue components and AJAX rendered elements // mainly for Vue components and AJAX rendered elements
for (const el of mutation.addedNodes) { for (const el of mutation.addedNodes) {
if (!isDocumentFragmentOrElementNode(el)) continue; if (elementNodeTypes.has(el.nodeType)) {
attachChildrenLazyTooltip(el); attachChildrenLazyTooltip(el);
if (el.hasAttribute('data-tooltip-content')) { if (el.hasAttribute('data-tooltip-content')) {
attachLazyTooltip(el); attachLazyTooltip(el);
}
} }
} }
} else if (mutation.type === 'attributes') { } else if (mutation.type === 'attributes') {

View File

@ -59,17 +59,6 @@ export function onDomReady(cb) {
} }
} }
// checks whether an element is owned by the current document, and whether it is a document fragment or element node
// if it is, it means it is a "normal" element managed by us, which can be modified safely.
export function isDocumentFragmentOrElementNode(el) {
try {
return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
} catch {
// in case the el is not in the same origin, then the access to nodeType would fail
return false;
}
}
// autosize a textarea to fit content. Based on // autosize a textarea to fit content. Based on
// https://github.com/github/textarea-autosize // https://github.com/github/textarea-autosize
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

View File

@ -1,21 +1,17 @@
// Convert an absolute or relative URL to an absolute URL with the current origin // Convert an absolute or relative URL to an absolute URL with the current origin
export function toOriginUrl(urlStr) {
try {
// only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx')
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
const {origin, protocol, hostname, port} = window.location;
const url = new URL(urlStr, origin);
url.protocol = protocol;
url.hostname = hostname;
url.port = port || (protocol === 'https:' ? '443' : '80');
return url.toString();
}
} catch {}
return urlStr;
}
window.customElements.define('gitea-origin-url', class extends HTMLElement { window.customElements.define('gitea-origin-url', class extends HTMLElement {
connectedCallback() { connectedCallback() {
this.textContent = toOriginUrl(this.getAttribute('data-url')); const urlStr = this.getAttribute('data-url');
try {
// only process absolute HTTP/HTTPS URL or relative URLs ('/xxx' or '//host/xxx')
if (urlStr.startsWith('http://') || urlStr.startsWith('https://') || urlStr.startsWith('/')) {
const url = new URL(urlStr, window.origin);
url.protocol = window.location.protocol;
url.host = window.location.host;
this.textContent = url.toString();
return;
}
} catch {}
this.textContent = urlStr;
} }
}); });

View File

@ -1,17 +0,0 @@
import {toOriginUrl} from './GiteaOriginUrl.js';
test('toOriginUrl', () => {
const oldLocation = window.location;
for (const origin of ['https://example.com', 'https://example.com:3000']) {
window.location = new URL(`${origin}/`);
expect(toOriginUrl('/')).toEqual(`${origin}/`);
expect(toOriginUrl('/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com/org/repo.git')).toEqual(`${origin}/org/repo.git`);
expect(toOriginUrl('https://another.com:4000')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/')).toEqual(`${origin}/`);
expect(toOriginUrl('https://another.com:4000/org/repo.git')).toEqual(`${origin}/org/repo.git`);
}
window.location = oldLocation;
});