Merge branch 'fix/dnd-independent-copy' of https://github.com/M-Hassan-Raza/kitty

This commit is contained in:
Kovid Goyal 2026-08-27 21:48:57 +05:30
commit dc4471ea6c
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 72 additions and 9 deletions

View file

@ -130,7 +130,8 @@ func uniqify_child_names(names []string, is_case_sensitive_filesystem bool) []st
return names
}
func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (err error) {
func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string, action int, copy_mode string) (err error) {
disallow_hardlinks := action == copy_on_drop && copy_mode == "independent"
var src_file *os.File
defer func() {
if src_file != nil {
@ -162,6 +163,7 @@ func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (e
return err
}
err = utils.CopyFolderContents(ctx, src_file, d, utils.CopyFolderOptions{
Disallow_hardlinks: disallow_hardlinks,
Filter_files: func(parent *os.File, child os.FileInfo) bool {
return child.IsDir() || child.Mode().IsRegular() || child.Mode()&fs.ModeSymlink != 0
},
@ -171,15 +173,17 @@ func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (e
return err
}
} else if st.Mode().IsRegular() {
// First try a hard link
dest := filepath.Join(dest_dir.Name(), filepath.Base(path))
if err = os.Link(path, dest); err == nil {
continue
if !disallow_hardlinks {
// First try a hard link
dest := filepath.Join(dest_dir.Name(), filepath.Base(path))
if err = os.Link(path, dest); err == nil {
continue
}
}
if src_file, err = os.Open(path); err != nil {
return err
}
d, err := utils.CreateAt(dest_dir, filepath.Base(dest), st.Mode().Perm())
d, err := utils.CreateAt(dest_dir, filepath.Base(path), st.Mode().Perm())
if err != nil {
return err
}
@ -202,7 +206,7 @@ func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (e
return
}
func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completion chan error, uri_list []string, wakeup func()) {
func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completion chan error, uri_list []string, action int, copy_mode string, wakeup func()) {
var err error
defer func() {
if r := recover(); r != nil {
@ -211,7 +215,7 @@ func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completi
completion <- err
wakeup()
}()
err = do_local_copy(ctx, dest_dir, uri_list)
err = do_local_copy(ctx, dest_dir, uri_list, action, copy_mode)
}
type path_stack struct {
@ -691,7 +695,7 @@ func (dnd *dnd) all_mime_data_dropped() (err error) {
}
drop_status.local_copy.ctx, drop_status.local_copy.cancel_ctx = context.WithCancel(context.Background())
drop_status.local_copy.completion = make(chan error, 1)
go do_local_copy_in_goroutine(drop_status.local_copy.ctx, f, drop_status.local_copy.completion, file_paths, func() { dnd.lp.WakeupMainThread() })
go do_local_copy_in_goroutine(drop_status.local_copy.ctx, f, drop_status.local_copy.completion, file_paths, drop_status.action, dnd.opts.CopyMode, func() { dnd.lp.WakeupMainThread() })
}
return
}

View file

@ -3,6 +3,7 @@
package dnd
import (
"context"
"os"
"path/filepath"
"sort"
@ -53,6 +54,54 @@ func sortedStrings(s []string) []string {
return out
}
func TestLocalCopyHardlinkPolicy(t *testing.T) {
for _, tc := range []struct {
name string
action int
copy_mode string
want_hardlink bool
}{
{"copy-auto", copy_on_drop, "auto", true},
{"copy-independent", copy_on_drop, "independent", false},
{"move-auto", move_on_drop, "auto", true},
{"move-independent", move_on_drop, "independent", true},
} {
t.Run(tc.name, func(t *testing.T) {
tmp := t.TempDir()
src := filepath.Join(tmp, "src")
dst := filepath.Join(tmp, "dst")
files := map[string]string{"file.txt": "top level", "folder/nested.txt": "nested"}
if err := os.Mkdir(dst, 0755); err != nil {
t.Fatal(err)
}
buildTree(t, src, files, nil)
if err := do_local_copy(context.Background(), openDir(t, dst), []string{filepath.Join(src, "file.txt"), filepath.Join(src, "folder")}, tc.action, tc.copy_mode); err != nil {
t.Fatalf("do_local_copy: %v", err)
}
for path, content := range files {
source_info, err := os.Stat(filepath.Join(src, path))
if err != nil {
t.Fatal(err)
}
copied := filepath.Join(dst, path)
copied_info, err := os.Stat(copied)
if err != nil {
t.Fatal(err)
}
if same := os.SameFile(source_info, copied_info); same != tc.want_hardlink {
t.Errorf("os.SameFile() for %s: got %v, want %v", path, same, tc.want_hardlink)
}
if data, err := os.ReadFile(copied); err != nil {
t.Fatal(err)
} else if string(data) != content {
t.Errorf("copied file %s has unexpected content", path)
}
}
})
}
}
// TestFindOverwrites_NoOverlap verifies that an empty result is returned when
// source and destination have no names in common.
func TestFindOverwrites_NoOverlap(t *testing.T) {

View file

@ -24,6 +24,16 @@ text/uri-list.
Path to the directory in which dropped data is saved. Defaults to the current working directory.
--copy-mode
type=choices
choices=auto,independent
default=auto
Control how local files are handled for Copy drops. The default, :code:`auto`, uses hard links
when possible for performance and falls back to copying file data. :code:`independent` always
copies file data, so modifying a dropped file cannot modify its source. Move drops always use
hard links when possible.
--confirm-drop-overwrite
type=bool-set
Ask for confirmation when dropping text/uri-list data if the drop will cause any existing