Add support for forking single branch (#25821)

Fixes #25117

Add UI for choosing branch to fork

Change default branch on single-branch forks


![image](https://github.com/go-gitea/gitea/assets/19504461/28505f69-a9a2-43a8-8b19-a0cdac3ddc5a)

---------

Co-authored-by: Denys Konovalov <kontakt@denyskon.de>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Dmitry Sharshakov 2023-09-29 04:48:39 +03:00 committed by GitHub
parent e8840e7e2b
commit 5e02e3b7ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 9 deletions

View file

@ -943,6 +943,8 @@ fork_from = Fork From
already_forked = You've already forked %s already_forked = You've already forked %s
fork_to_different_account = Fork to a different account fork_to_different_account = Fork to a different account
fork_visibility_helper = The visibility of a forked repository cannot be changed. fork_visibility_helper = The visibility of a forked repository cannot be changed.
fork_branch = Branch to be cloned to the fork
all_branches = All branches
fork_no_valid_owners = This repository can not be forked because there are no valid owners. fork_no_valid_owners = This repository can not be forked because there are no valid owners.
use_template = Use this template use_template = Use this template
clone_in_vsc = Clone in VS Code clone_in_vsc = Clone in VS Code

View file

@ -180,6 +180,21 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
return nil return nil
} }
branches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
RepoID: ctx.Repo.Repository.ID,
ListOptions: db.ListOptions{
ListAll: true,
},
IsDeletedBranch: util.OptionalBoolFalse,
// Add it as the first option
ExcludeBranchNames: []string{ctx.Repo.Repository.DefaultBranch},
})
if err != nil {
ctx.ServerError("FindBranchNames", err)
return nil
}
ctx.Data["Branches"] = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
return forkRepo return forkRepo
} }
@ -264,6 +279,7 @@ func ForkPost(ctx *context.Context) {
BaseRepo: forkRepo, BaseRepo: forkRepo,
Name: form.RepoName, Name: form.RepoName,
Description: form.Description, Description: form.Description,
SingleBranch: form.ForkSingleBranch,
}) })
if err != nil { if err != nil {
ctx.Data["Err_RepoName"] = true ctx.Data["Err_RepoName"] = true

View file

@ -51,6 +51,8 @@ type CreateRepoForm struct {
Labels bool Labels bool
ProtectedBranch bool ProtectedBranch bool
TrustModel string TrustModel string
ForkSingleBranch string
} }
// Validate validates the fields // Validate validates the fields

View file

@ -47,6 +47,7 @@ type ForkRepoOptions struct {
BaseRepo *repo_model.Repository BaseRepo *repo_model.Repository
Name string Name string
Description string Description string
SingleBranch string
} }
// ForkRepository forks a repository // ForkRepository forks a repository
@ -70,6 +71,10 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
} }
} }
defaultBranch := opts.BaseRepo.DefaultBranch
if opts.SingleBranch != "" {
defaultBranch = opts.SingleBranch
}
repo := &repo_model.Repository{ repo := &repo_model.Repository{
OwnerID: owner.ID, OwnerID: owner.ID,
Owner: owner, Owner: owner,
@ -77,7 +82,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
Name: opts.Name, Name: opts.Name,
LowerName: strings.ToLower(opts.Name), LowerName: strings.ToLower(opts.Name),
Description: opts.Description, Description: opts.Description,
DefaultBranch: opts.BaseRepo.DefaultBranch, DefaultBranch: defaultBranch,
IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate, IsPrivate: opts.BaseRepo.IsPrivate || opts.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate,
IsEmpty: opts.BaseRepo.IsEmpty, IsEmpty: opts.BaseRepo.IsEmpty,
IsFork: true, IsFork: true,
@ -134,9 +139,12 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
needsRollback = true needsRollback = true
cloneCmd := git.NewCommand(txCtx, "clone", "--bare")
if opts.SingleBranch != "" {
cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
}
repoPath := repo_model.RepoPath(owner.Name, repo.Name) repoPath := repo_model.RepoPath(owner.Name, repo.Name)
if stdout, _, err := git.NewCommand(txCtx, if stdout, _, err := cloneCmd.AddDynamicArguments(oldRepoPath, repoPath).
"clone", "--bare").AddDynamicArguments(oldRepoPath, repoPath).
SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())). SetDescription(fmt.Sprintf("ForkRepository(git clone): %s to %s", opts.BaseRepo.FullName(), repo.FullName())).
RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil { RunStdBytes(&git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err) log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)

View file

@ -51,6 +51,26 @@
</div> </div>
<span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span> <span class="help">{{ctx.Locale.Tr "repo.fork_visibility_helper"}}</span>
</div> </div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.fork_branch"}}</label>
<div class="ui selection dropdown">
<input type="hidden" id="fork_single_branch" name="fork_single_branch" value="" required>
<span class="text truncated-item-container" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}">
<span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span>
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item truncated-item-container" data-value="" title="{{ctx.Locale.Tr "repo.all_branches"}}">
<span class="truncated-item-name">{{ctx.Locale.Tr "repo.all_branches"}}</span>
</div>
{{range .Branches}}
<div class="item truncated-item-container" data-value="{{.}}" title="{{.}}">
<span class="truncated-item-name">{{.}}</span>
</div>
{{end}}
</div>
</div>
</div>
<div class="inline field {{if .Err_Description}}error{{end}}"> <div class="inline field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label> <label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
<textarea id="description" name="description">{{.description}}</textarea> <textarea id="description" name="description">{{.description}}</textarea>