1package lfs
2
3import (
4	"fmt"
5	"io"
6	"os"
7	"path/filepath"
8	"runtime"
9	"strings"
10
11	"github.com/git-lfs/git-lfs/v3/config"
12	"github.com/git-lfs/git-lfs/v3/tools"
13)
14
15type Platform int
16
17const (
18	PlatformWindows      = Platform(iota)
19	PlatformLinux        = Platform(iota)
20	PlatformOSX          = Platform(iota)
21	PlatformOther        = Platform(iota) // most likely a *nix variant e.g. freebsd
22	PlatformUndetermined = Platform(iota)
23)
24
25var currentPlatform = PlatformUndetermined
26
27func join(parts ...string) string {
28	return strings.Join(parts, "/")
29}
30
31func (f *GitFilter) CopyCallbackFile(event, filename string, index, totalFiles int) (tools.CopyCallback, *os.File, error) {
32	logPath, _ := f.cfg.Os.Get("GIT_LFS_PROGRESS")
33	if len(logPath) == 0 || len(filename) == 0 || len(event) == 0 {
34		return nil, nil, nil
35	}
36
37	if !filepath.IsAbs(logPath) {
38		return nil, nil, fmt.Errorf("GIT_LFS_PROGRESS must be an absolute path")
39	}
40
41	cbDir := filepath.Dir(logPath)
42	if err := tools.MkdirAll(cbDir, f.cfg); err != nil {
43		return nil, nil, wrapProgressError(err, event, logPath)
44	}
45
46	file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
47	if err != nil {
48		return nil, file, wrapProgressError(err, event, logPath)
49	}
50
51	var prevWritten int64
52
53	cb := tools.CopyCallback(func(total int64, written int64, current int) error {
54		if written != prevWritten {
55			_, err := file.Write([]byte(fmt.Sprintf("%s %d/%d %d/%d %s\n", event, index, totalFiles, written, total, filename)))
56			file.Sync()
57			prevWritten = written
58			return wrapProgressError(err, event, logPath)
59		}
60
61		return nil
62	})
63
64	return cb, file, nil
65}
66
67func wrapProgressError(err error, event, filename string) error {
68	if err != nil {
69		return fmt.Errorf("error writing Git LFS %s progress to %s: %s", event, filename, err.Error())
70	}
71
72	return nil
73}
74
75var localDirSet = tools.NewStringSetFromSlice([]string{".", "./", ".\\"})
76
77func GetPlatform() Platform {
78	if currentPlatform == PlatformUndetermined {
79		switch runtime.GOOS {
80		case "windows":
81			currentPlatform = PlatformWindows
82		case "linux":
83			currentPlatform = PlatformLinux
84		case "darwin":
85			currentPlatform = PlatformOSX
86		default:
87			currentPlatform = PlatformOther
88		}
89	}
90	return currentPlatform
91}
92
93type PathConverter interface {
94	Convert(string) string
95}
96
97// Convert filenames expressed relative to the root of the repo relative to the
98// current working dir. Useful when needing to calling git with results from a rooted command,
99// but the user is in a subdir of their repo
100func NewRepoToCurrentPathConverter(cfg *config.Configuration) (PathConverter, error) {
101	r, c, p, err := pathConverterArgs(cfg)
102	if err != nil {
103		return nil, err
104	}
105
106	return &repoToCurrentPathConverter{
107		repoDir:     r,
108		currDir:     c,
109		passthrough: p,
110	}, nil
111}
112
113type repoToCurrentPathConverter struct {
114	repoDir     string
115	currDir     string
116	passthrough bool
117}
118
119func (p *repoToCurrentPathConverter) Convert(filename string) string {
120	if p.passthrough {
121		return filename
122	}
123
124	abs := join(p.repoDir, filename)
125	rel, err := filepath.Rel(p.currDir, abs)
126	if err != nil {
127		// Use absolute file instead
128		return abs
129	}
130	return filepath.ToSlash(rel)
131}
132
133// Convert filenames expressed relative to the current directory to be
134// relative to the repo root. Useful when calling git with arguments that requires them
135// to be rooted but the user is in a subdir of their repo & expects to use relative args
136func NewCurrentToRepoPathConverter(cfg *config.Configuration) (PathConverter, error) {
137	r, c, p, err := pathConverterArgs(cfg)
138	if err != nil {
139		return nil, err
140	}
141
142	return &currentToRepoPathConverter{
143		repoDir:     r,
144		currDir:     c,
145		passthrough: p,
146	}, nil
147}
148
149type currentToRepoPathConverter struct {
150	repoDir     string
151	currDir     string
152	passthrough bool
153}
154
155func (p *currentToRepoPathConverter) Convert(filename string) string {
156	if p.passthrough {
157		return filename
158	}
159
160	var abs string
161	if filepath.IsAbs(filename) {
162		abs = tools.ResolveSymlinks(filename)
163	} else {
164		abs = join(p.currDir, filename)
165	}
166	reltoroot, err := filepath.Rel(p.repoDir, abs)
167	if err != nil {
168		// Can't do this, use absolute as best fallback
169		return abs
170	}
171	return filepath.ToSlash(reltoroot)
172}
173
174// Convert filenames expressed relative to the current directory to be relative
175// to the repo root and convert them into wildmatch patterns.
176func NewCurrentToRepoPatternConverter(cfg *config.Configuration) (PathConverter, error) {
177	r, c, p, err := pathConverterArgs(cfg)
178	if err != nil {
179		return nil, err
180	}
181
182	return &currentToRepoPatternConverter{
183		c: &currentToRepoPathConverter{
184			repoDir:     r,
185			currDir:     c,
186			passthrough: p,
187		},
188	}, nil
189}
190
191type currentToRepoPatternConverter struct {
192	c *currentToRepoPathConverter
193}
194
195func (p *currentToRepoPatternConverter) Convert(filename string) string {
196	pattern := p.c.Convert(filename)
197	if st, err := os.Stat(filename); err == nil && st.IsDir() {
198		pattern += "/"
199	}
200	if strings.HasPrefix(pattern, "./") {
201		pattern = pattern[2:]
202		if len(pattern) == 0 {
203			pattern = "**"
204		}
205	}
206	return pattern
207}
208
209func pathConverterArgs(cfg *config.Configuration) (string, string, bool, error) {
210	currDir, err := os.Getwd()
211	if err != nil {
212		return "", "", false, fmt.Errorf("unable to get working dir: %v", err)
213	}
214	currDir = tools.ResolveSymlinks(currDir)
215	return cfg.LocalWorkingDir(), currDir, cfg.LocalWorkingDir() == currDir, nil
216}
217
218// Are we running on Windows? Need to handle some extra path shenanigans
219func IsWindows() bool {
220	return GetPlatform() == PlatformWindows
221}
222
223func CopyFileContents(cfg *config.Configuration, src string, dst string) error {
224	tmp, err := TempFile(cfg, filepath.Base(dst))
225	if err != nil {
226		return err
227	}
228	defer func() {
229		tmp.Close()
230		os.Remove(tmp.Name())
231	}()
232	in, err := os.Open(src)
233	if err != nil {
234		return err
235	}
236	defer in.Close()
237	_, err = io.Copy(tmp, in)
238	if err != nil {
239		return err
240	}
241	err = tmp.Close()
242	if err != nil {
243		return err
244	}
245	return os.Rename(tmp.Name(), dst)
246}
247
248func LinkOrCopy(cfg *config.Configuration, src string, dst string) error {
249	if src == dst {
250		return nil
251	}
252	err := os.Link(src, dst)
253	if err == nil {
254		return err
255	}
256	return CopyFileContents(cfg, src, dst)
257}
258
259// TempFile creates a temporary file in the temporary directory specified by the
260// configuration that has the proper permissions for the repository.  On
261// success, it returns an open, non-nil *os.File, and the caller is responsible
262// for closing and/or removing it.  On failure, the temporary file is
263// automatically cleaned up and an error returned.
264//
265// This function is designed to handle only temporary files that will be renamed
266// into place later somewhere within the Git repository.
267func TempFile(cfg *config.Configuration, pattern string) (*os.File, error) {
268	return tools.TempFile(cfg.TempDir(), pattern, cfg)
269}
270