jojo/tests/integration/mirror_pull_test.go
Mathieu Fenniak 8cb776dcac chore: fix TestMirrorPull on older git (2.34.1) installation (#12134)
`TestMirrorPull` is currently failing when run on git 2.34.1 in the `testing-integration.yml` workflow: https://codeberg.org/forgejo-integration/forgejo/actions/runs/16661/jobs/1/attempt/1#jobstep-5-2539  Began to fail after #11909 when additional checks on pull mirror configuration was added.

This PR addresses the issue and has been manually tested against the same git version:
```
$ git --version
git version 2.34.1

$ make test-sqlite#TestMirrorPull 2>&1
...
=== TestMirrorPull/migrate_from_repo_config_credentials (tests/integration/mirror_pull_test.go:238)
PASS
```

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12134
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Reviewed-by: Beowulf <beowulf@beocode.eu>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2026-04-16 02:08:43 +02:00

579 lines
20 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"os"
"path"
"strings"
"testing"
"time"
"forgejo.org/models/auth"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/migration"
"forgejo.org/modules/optional"
"forgejo.org/modules/process"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
app_context "forgejo.org/services/context"
"forgejo.org/services/forms"
"forgejo.org/services/migrations"
mirror_service "forgejo.org/services/mirror"
release_service "forgejo.org/services/release"
repo_service "forgejo.org/services/repository"
files_service "forgejo.org/services/repository/files"
"forgejo.org/tests"
"github.com/PuerkitoBio/goquery"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMirrorPull(t *testing.T) {
t.Run("Basic", func(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
opts := migration.MigrateOptions{
RepoName: "test_mirror",
Description: "Test mirror",
Private: false,
Mirror: true,
CloneAddr: repoPath,
Wiki: true,
Releases: false,
}
mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
IsPrivate: opts.Private,
IsMirror: opts.Mirror,
Status: repo_model.RepositoryBeingMigrated,
})
require.NoError(t, err)
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
ctx := t.Context()
mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
require.NoError(t, err)
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
require.NoError(t, err)
defer gitRepo.Close()
findOptions := repo_model.FindReleasesOptions{
IncludeDrafts: true,
IncludeTags: true,
RepoID: mirror.ID,
}
initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
require.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.2",
Target: "master",
Title: "v0.2 is released",
Note: "v0.2 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: true,
}, "", []*release_service.AttachmentChange{}))
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
require.NoError(t, err)
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)
count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
assert.Equal(t, initCount+1, count)
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v0.2")
require.NoError(t, err)
require.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true))
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)
count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
assert.Equal(t, initCount, count)
})
// How will we interact with the pull mirror during this test?
interactionMethod := []struct {
name string
useAPI bool
createPullMirror func(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) (repoName string)
verifyMirrorDetails func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
triggerPullMirror func(t *testing.T, mirrorName string)
changePullMirrorCredentials func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
changePullMirrorAddress func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
}{
{
name: "API",
useAPI: true,
createPullMirror: createPullMirrorViaAPI,
triggerPullMirror: triggerPullMirrorViaAPI,
verifyMirrorDetails: func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
// API provides no visibility into a repo's mirror settings right now
},
},
{
name: "Web",
useAPI: false,
createPullMirror: createPullMirrorViaWeb,
triggerPullMirror: triggerPullMirrorViaWeb,
verifyMirrorDetails: verifyPullMirrorViaWeb,
changePullMirrorCredentials: changePullMirrorCredentialsViaWeb,
changePullMirrorAddress: changePullMirrorCredentialsViaWeb, // one endpoint, so same as creds
},
}
mirrorConfiguration := []struct {
name string
privateSource bool
}{
{
name: "HTTP Without Auth",
},
{
name: "HTTP With Auth",
privateSource: true,
},
}
// Not using MockVariableValue due to need to undo `migrations.Init()`
prev := setting.Migrations.AllowedDomains
setting.Migrations.AllowedDomains = "localhost"
migrations.Init() // reinitialize for changed allowList
defer func() {
setting.Migrations.AllowedDomains = prev
migrations.Init() // reinitialize for changed allowList
}()
onApplicationRun(t, func(t *testing.T, u *url.URL) {
for _, im := range interactionMethod {
for _, mc := range mirrorConfiguration {
t.Run(fmt.Sprintf("%s/%s", im.name, mc.name), func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create the source repository that will be mirrored.
sourceRepo, sourceRepoSha, cleanupSource := tests.CreateDeclarativeRepoWithOptions(t, user2,
tests.DeclarativeRepoOptions{
IsPrivate: optional.Some(mc.privateSource),
Files: optional.Some([]*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "docs.md",
ContentReader: strings.NewReader("hello, world"),
},
}),
},
)
defer cleanupSource()
require.NotEmpty(t, sourceRepoSha)
// Create pull mirror
mirror := im.createPullMirror(t, sourceRepo, mc.privateSource)
verifyPullMirrorContents(t, mirror, sourceRepoSha)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
// Push a change to the source and refresh the mirror
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
// Test changing the mirror's authentication method (if available)
if mc.privateSource && im.changePullMirrorCredentials != nil {
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.changePullMirrorCredentials(t, sourceRepo, mirror, mc.privateSource)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
}
// Test changing the mirror's address (if available)
if im.changePullMirrorAddress != nil {
sourceRepo = renamePullMirrorSourceRepo(t, sourceRepo)
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.changePullMirrorAddress(t, sourceRepo, mirror, mc.privateSource)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
}
})
}
}
})
t.Run("migrate from repo config credentials", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
mirrorRepo, _, cleanupMirror := tests.CreateDeclarativeRepoWithOptions(t, user2,
tests.DeclarativeRepoOptions{},
)
defer cleanupMirror()
// Write to the repo a config file that would have plausibly existed before EncryptedRemoteAddress was
// introduced:
repoPath := mirrorRepo.RepoPath()
err := os.WriteFile(path.Join(repoPath, "config"), []byte(`
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://user:password@example.com/org/repo.git
tagOpt = --no-tags
fetch = +refs/*:refs/*
mirror = true
fetch = +refs/tags/*:refs/tags/*
`), 0o644)
require.NoError(t, err)
// Create a Mirror record without an EncryptedRemoteAddress:
mirror := &repo_model.Mirror{
RepoID: mirrorRepo.ID,
Interval: 8 * time.Hour,
EnablePrune: true,
}
_, err = db.GetEngine(t.Context()).Insert(mirror)
require.NoError(t, err)
require.Nil(t, mirror.EncryptedRemoteAddress)
remoteURL, err := mirror_service.DecryptOrRecoverRemoteAddress(t.Context(), mirror)
require.NoError(t, err)
assert.Equal(t, "https://user:password@example.com/org/repo.git", remoteURL.URL.String())
// EncryptedRemoteAddress should now be populated from the recovery:
assert.NotNil(t, mirror.EncryptedRemoteAddress)
maybeDecryptedURL, err := mirror.DecryptRemoteAddress()
require.NoError(t, err)
has, decryptedURL := maybeDecryptedURL.Get()
require.True(t, has)
assert.Equal(t, "https://user:password@example.com/org/repo.git", decryptedURL)
// SanitizedRemoteAddress can be fetched:
maybeSanitizedURL, err := mirror.SanitizedRemoteAddress()
require.NoError(t, err)
has, sanitizedURL := maybeSanitizedURL.Get()
require.True(t, has)
assert.Equal(t, "https://user@example.com/org/repo.git", sanitizedURL)
// Database record is updated in the database:
refetchMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID})
assert.Equal(t, mirror.EncryptedRemoteAddress, refetchMirror.EncryptedRemoteAddress)
// Config file is rewritten:
config, err := os.ReadFile(path.Join(repoPath, "config"))
require.NoError(t, err)
assert.Equal(t, `
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://user@example.com/org/repo.git
tagOpt = --no-tags
fetch = +refs/*:refs/*
mirror = true
fetch = +refs/tags/*:refs/tags/*
`, string(config))
})
}
func createPullMirrorViaWeb(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) string {
session := loginUser(t, "user2")
mirrorName := fmt.Sprintf("pullmirror-%s", sourceRepo.Name)
form := &forms.MigrateRepoForm{
CloneAddr: sourceRepo.CloneLink().HTTPS,
Service: structs.PlainGitService,
UID: 2,
RepoName: mirrorName,
Mirror: true,
}
if authenticate {
form.AuthUsername = "user2"
form.AuthPassword = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithJSON(t, "POST", "/repo/migrate", form),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/pullmirror-%s", sourceRepo.Name), location)
var lastBody string
if !assert.Eventuallyf(t,
func() bool {
resp := session.MakeRequest(t,
NewRequest(t, "GET", location),
http.StatusOK)
body := resp.Body.String()
lastBody = body
// Looking for the repo page to be fully populated indicating that the migration is complete:
// Check that the first commit message is present:
if !strings.Contains(body, "Initial commit") {
return false
}
// Check that the fork button is present:
if !strings.Contains(body, fmt.Sprintf("/user2/%s/fork", mirrorName)) {
return false
}
return true
},
15*time.Second, 1*time.Second,
"expected migration to complete and repo page to render") {
t.Logf("last received page body: %s", lastBody)
}
return mirrorName
}
func createPullMirrorViaAPI(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) string {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
mirrorName := fmt.Sprintf("pullmirror-%s", sourceRepo.Name)
form := &structs.MigrateRepoOptions{
CloneAddr: sourceRepo.CloneLink().HTTPS,
Service: "git",
RepoOwner: "user2",
RepoName: mirrorName,
Mirror: true,
}
if authenticate {
form.AuthUsername = "user2"
form.AuthPassword = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", form).AddTokenAuth(apiToken),
http.StatusCreated)
var repo structs.Repository
DecodeJSON(t, resp, &repo)
assert.NotNil(t, repo)
assert.True(t, repo.Mirror)
assert.False(t, repo.Empty)
return mirrorName
}
func verifyPullMirrorViaWeb(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
session := loginUser(t, "user2")
resp := session.MakeRequest(t,
NewRequestf(t, "GET", "/user2/%s/settings", mirrorName),
http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertAttrEqual(t, "#mirror_address", "value", sourceRepo.CloneLink().HTTPS)
if authenticate {
htmlDoc.AssertAttrEqual(t, "#mirror_username", "value", "user2")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "placeholder", "(Unchanged)")
} else {
htmlDoc.AssertAttrEqual(t, "#mirror_username", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "placeholder", "(Unset)")
}
resp = session.MakeRequest(t,
NewRequestf(t, "GET", "/user2/%s", mirrorName),
http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
htmlDoc.AssertElementPredicate(t, ".fork-flag", func(selection *goquery.Selection) bool {
text := strings.TrimSpace(selection.Text())
assert.Contains(t, text, "mirror of")
assert.Contains(t, text, sourceRepo.CloneLink().HTTPS)
return true
})
}
func triggerPullMirrorViaWeb(t *testing.T, mirrorName string) {
session := loginUser(t, "user2")
resp := session.MakeRequest(t,
NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/%s/settings", mirrorName), map[string]string{"action": "mirror-sync"}),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/%s/settings", mirrorName), location)
}
func triggerPullMirrorViaAPI(t *testing.T, mirrorName string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
// Trigger sync...
session.MakeRequest(t,
NewRequestf(t, "POST", "/api/v1/repos/user2/%s/mirror-sync", mirrorName).AddTokenAuth(apiToken),
http.StatusOK)
}
func changePullMirrorCredentialsViaWeb(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
session := loginUser(t, "user2")
form := map[string]string{
"action": "mirror",
"enable_prune": "on",
"interval": "8h0m0s",
"mirror_address": sourceRepo.CloneLink().HTTPS,
}
if authenticate {
form["mirror_username"] = "user2"
form["mirror_password"] = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/%s/settings", mirrorName), form),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/%s/settings", mirrorName), location)
}
func verifyPullMirrorContents(t *testing.T, mirrorName, expectedSha string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
resp := session.MakeRequest(t,
NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/%s/commits?sha=main&limit=1", mirrorName)).AddTokenAuth(apiToken),
http.StatusOK)
var commits []*structs.Commit
DecodeJSON(t, resp, &commits)
require.Len(t, commits, 1)
assert.Equal(t, expectedSha, commits[0].SHA)
}
func waitForPullMirror(t *testing.T, mirrorName, expectedSha string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
var commits []*structs.Commit
if !assert.Eventually(t,
func() bool {
resp := session.MakeRequest(t,
NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/%s/commits?sha=main&limit=1", mirrorName)).AddTokenAuth(apiToken),
http.StatusOK)
DecodeJSON(t, resp, &commits)
require.Len(t, commits, 1)
return commits[0].SHA == expectedSha
},
15*time.Second, 1*time.Second) {
t.Logf("sync was supposed to bring repo to commit %s, but observed commits = %#v", expectedSha, commits)
}
}
func getGitConfig(t *testing.T, configFile, configPath string) string {
stdout, stderr, err := process.GetManager().Exec("getGitConfig", "git", "config", "--get", "--file", configFile, configPath)
require.NoError(t, err, "fetch config %s failed: git stderr: %s", configPath, stderr)
return strings.TrimSpace(stdout)
}
func verifyPullMirrorConfig(t *testing.T, mirrorName string, sourceRepo *repo_model.Repository, authenticate bool) {
mirrorRepo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", mirrorName)
require.NoError(t, err)
repoPath := mirrorRepo.RepoPath()
configPath := path.Join(repoPath, "config")
expectedURL := sourceRepo.CloneLink().HTTPS
if authenticate {
expectedURL = strings.Replace(expectedURL, "http://", "http://user2@", 1)
}
assert.Equal(t, expectedURL, getGitConfig(t, configPath, "remote.origin.url"))
assert.Equal(t, "true", getGitConfig(t, configPath, "remote.origin.mirror"))
assert.Equal(t, "+refs/tags/*:refs/tags/*", getGitConfig(t, configPath, "remote.origin.fetch"))
}
func changePullMirrorSource(t *testing.T, sourceRepo *repo_model.Repository, sourceRepoSha string) string {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
resp, err := files_service.ChangeRepoFiles(git.DefaultContext, sourceRepo, user2,
&files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "update",
TreePath: "docs.md",
ContentReader: strings.NewReader(uuid.NewString()),
},
},
Message: "add files",
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
LastCommitID: sourceRepoSha,
})
require.NoError(t, err)
assert.NotEmpty(t, resp)
return resp.Commit.SHA
}
func renamePullMirrorSourceRepo(t *testing.T, sourceRepo *repo_model.Repository) *repo_model.Repository {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
newName := uuid.NewString()
session.MakeRequest(t,
NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/user2/%s", sourceRepo.Name),
&structs.EditRepoOption{
Name: &newName,
}).AddTokenAuth(apiToken),
http.StatusOK)
newRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: sourceRepo.ID})
assert.Equal(t, newRepo.Name, newName)
assert.NotEqual(t, newRepo.CloneLink().HTTPS, sourceRepo.CloneLink().HTTPS) // should have changed to new name
return newRepo
}
func TestPullMirrorRedactCredentials(t *testing.T) {
defer unittest.OverrideFixtures("tests/integration/fixtures/TestPullMirrorRedactCredentials")()
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1001/settings", map[string]string{
"action": "mirror-sync",
}), http.StatusSeeOther)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "info%3DPulling%2Bchanges%2Bfrom%2Bthe%2Bremote%2Bhttps%253A%252F%252Fexample.com%252Fexample%252Fexample.git%2Bat%2Bthe%2Bmoment.", flashCookie.Value)
}