1package commands
2
3import (
4	"fmt"
5	"io"
6	"io/ioutil"
7	"net/url"
8	"os"
9	"path/filepath"
10	"strings"
11	"sync"
12
13	"github.com/git-lfs/git-lfs/v3/errors"
14	"github.com/git-lfs/git-lfs/v3/git"
15	"github.com/git-lfs/git-lfs/v3/lfs"
16	"github.com/git-lfs/git-lfs/v3/tasklog"
17	"github.com/git-lfs/git-lfs/v3/tools"
18	"github.com/git-lfs/git-lfs/v3/tq"
19	"github.com/rubyist/tracerx"
20)
21
22func uploadForRefUpdates(ctx *uploadContext, updates []*git.RefUpdate, pushAll bool) error {
23	gitscanner, err := ctx.buildGitScanner()
24	if err != nil {
25		return err
26	}
27
28	defer func() {
29		gitscanner.Close()
30		ctx.ReportErrors()
31	}()
32
33	verifyLocksForUpdates(ctx.lockVerifier, updates)
34	rightSides := make([]string, 0, len(updates))
35	for _, update := range updates {
36		right := update.Right().Sha
37		if update.LeftCommitish() != right {
38			rightSides = append(rightSides, right)
39		}
40	}
41	for _, update := range updates {
42		// initialized here to prevent looped defer
43		q := ctx.NewQueue(
44			tq.RemoteRef(update.Right()),
45		)
46		err := uploadLeftOrAll(gitscanner, ctx, q, rightSides, update, pushAll)
47		ctx.CollectErrors(q)
48
49		if err != nil {
50			return errors.Wrap(err, fmt.Sprintf("ref %s:", update.Left().Name))
51		}
52	}
53
54	return nil
55}
56
57func uploadLeftOrAll(g *lfs.GitScanner, ctx *uploadContext, q *tq.TransferQueue, bases []string, update *git.RefUpdate, pushAll bool) error {
58	cb := ctx.gitScannerCallback(q)
59	if pushAll {
60		if err := g.ScanRefWithDeleted(update.LeftCommitish(), cb); err != nil {
61			return err
62		}
63	} else {
64		left := update.LeftCommitish()
65		right := update.Right().Sha
66		if left == right {
67			right = ""
68		}
69		if err := g.ScanMultiRangeToRemote(left, bases, cb); err != nil {
70			return err
71		}
72	}
73	return ctx.scannerError()
74}
75
76type uploadContext struct {
77	Remote       string
78	DryRun       bool
79	Manifest     *tq.Manifest
80	uploadedOids tools.StringSet
81	gitfilter    *lfs.GitFilter
82
83	logger *tasklog.Logger
84	meter  *tq.Meter
85
86	committerName  string
87	committerEmail string
88
89	lockVerifier *lockVerifier
90
91	// allowMissing specifies whether pushes containing missing/corrupt
92	// pointers should allow pushing Git blobs
93	allowMissing bool
94
95	// tracks errors from gitscanner callbacks
96	scannerErr error
97	errMu      sync.Mutex
98
99	// filename => oid
100	missing   map[string]string
101	corrupt   map[string]string
102	otherErrs []error
103}
104
105func newUploadContext(dryRun bool) *uploadContext {
106	remote := cfg.PushRemote()
107	manifest := getTransferManifestOperationRemote("upload", remote)
108	ctx := &uploadContext{
109		Remote:       remote,
110		Manifest:     manifest,
111		DryRun:       dryRun,
112		uploadedOids: tools.NewStringSet(),
113		gitfilter:    lfs.NewGitFilter(cfg),
114		lockVerifier: newLockVerifier(manifest),
115		allowMissing: cfg.Git.Bool("lfs.allowincompletepush", false),
116		missing:      make(map[string]string),
117		corrupt:      make(map[string]string),
118		otherErrs:    make([]error, 0),
119	}
120
121	var sink io.Writer = os.Stdout
122	if dryRun {
123		sink = ioutil.Discard
124	}
125
126	ctx.logger = tasklog.NewLogger(sink,
127		tasklog.ForceProgress(cfg.ForceProgress()),
128	)
129	ctx.meter = buildProgressMeter(ctx.DryRun, tq.Upload)
130	ctx.logger.Enqueue(ctx.meter)
131	ctx.committerName, ctx.committerEmail = cfg.CurrentCommitter()
132	return ctx
133}
134
135func (c *uploadContext) NewQueue(options ...tq.Option) *tq.TransferQueue {
136	return tq.NewTransferQueue(tq.Upload, c.Manifest, c.Remote, append(options,
137		tq.DryRun(c.DryRun),
138		tq.WithProgress(c.meter),
139	)...)
140}
141
142func (c *uploadContext) scannerError() error {
143	c.errMu.Lock()
144	defer c.errMu.Unlock()
145
146	return c.scannerErr
147}
148
149func (c *uploadContext) addScannerError(err error) {
150	c.errMu.Lock()
151	defer c.errMu.Unlock()
152
153	if c.scannerErr != nil {
154		c.scannerErr = fmt.Errorf("%v\n%v", c.scannerErr, err)
155	} else {
156		c.scannerErr = err
157	}
158}
159
160func (c *uploadContext) buildGitScanner() (*lfs.GitScanner, error) {
161	gitscanner := lfs.NewGitScanner(cfg, nil)
162	gitscanner.FoundLockable = func(n string) { c.lockVerifier.LockedByThem(n) }
163	gitscanner.PotentialLockables = c.lockVerifier
164	return gitscanner, gitscanner.RemoteForPush(c.Remote)
165}
166
167func (c *uploadContext) gitScannerCallback(tqueue *tq.TransferQueue) func(*lfs.WrappedPointer, error) {
168	return func(p *lfs.WrappedPointer, err error) {
169		if err != nil {
170			c.addScannerError(err)
171		} else {
172			c.UploadPointers(tqueue, p)
173		}
174	}
175}
176
177// AddUpload adds the given oid to the set of oids that have been uploaded in
178// the current process.
179func (c *uploadContext) SetUploaded(oid string) {
180	c.uploadedOids.Add(oid)
181}
182
183// HasUploaded determines if the given oid has already been uploaded in the
184// current process.
185func (c *uploadContext) HasUploaded(oid string) bool {
186	return c.uploadedOids.Contains(oid)
187}
188
189func (c *uploadContext) prepareUpload(unfiltered ...*lfs.WrappedPointer) []*lfs.WrappedPointer {
190	numUnfiltered := len(unfiltered)
191	uploadables := make([]*lfs.WrappedPointer, 0, numUnfiltered)
192
193	// XXX(taylor): temporary measure to fix duplicate (broken) results from
194	// scanner
195	uniqOids := tools.NewStringSet()
196
197	// separate out objects that _should_ be uploaded, but don't exist in
198	// .git/lfs/objects. Those will skipped if the server already has them.
199	for _, p := range unfiltered {
200		// object already uploaded in this process, or we've already
201		// seen this OID (see above), skip!
202		if uniqOids.Contains(p.Oid) || c.HasUploaded(p.Oid) || p.Size == 0 {
203			continue
204		}
205		uniqOids.Add(p.Oid)
206
207		// canUpload determines whether the current pointer "p" can be
208		// uploaded through the TransferQueue below. It is set to false
209		// only when the file is locked by someone other than the
210		// current committer.
211		var canUpload bool = true
212
213		if c.lockVerifier.LockedByThem(p.Name) {
214			// If the verification state is enabled, this failed
215			// locks verification means that the push should fail.
216			//
217			// If the state is disabled, the verification error is
218			// silent and the user can upload.
219			//
220			// If the state is undefined, the verification error is
221			// sent as a warning and the user can upload.
222			canUpload = !c.lockVerifier.Enabled()
223		}
224
225		c.lockVerifier.LockedByUs(p.Name)
226
227		if canUpload {
228			// estimate in meter early (even if it's not going into
229			// uploadables), since we will call Skip() based on the
230			// results of the download check queue.
231			c.meter.Add(p.Size)
232
233			uploadables = append(uploadables, p)
234		}
235	}
236
237	return uploadables
238}
239
240func (c *uploadContext) UploadPointers(q *tq.TransferQueue, unfiltered ...*lfs.WrappedPointer) {
241	if c.DryRun {
242		for _, p := range unfiltered {
243			if c.HasUploaded(p.Oid) {
244				continue
245			}
246
247			Print("push %s => %s", p.Oid, p.Name)
248			c.SetUploaded(p.Oid)
249		}
250
251		return
252	}
253
254	pointers := c.prepareUpload(unfiltered...)
255	for _, p := range pointers {
256		t, err := c.uploadTransfer(p)
257		if err != nil && !errors.IsCleanPointerError(err) {
258			ExitWithError(err)
259		}
260
261		q.Add(t.Name, t.Path, t.Oid, t.Size, t.Missing, nil)
262		c.SetUploaded(p.Oid)
263	}
264}
265
266func (c *uploadContext) CollectErrors(tqueue *tq.TransferQueue) {
267	tqueue.Wait()
268
269	for _, err := range tqueue.Errors() {
270		if malformed, ok := err.(*tq.MalformedObjectError); ok {
271			if malformed.Missing() {
272				c.missing[malformed.Name] = malformed.Oid
273			} else if malformed.Corrupt() {
274				c.corrupt[malformed.Name] = malformed.Oid
275			}
276		} else {
277			c.otherErrs = append(c.otherErrs, err)
278		}
279	}
280}
281
282func (c *uploadContext) ReportErrors() {
283	c.meter.Finish()
284
285	for _, err := range c.otherErrs {
286		FullError(err)
287	}
288
289	if len(c.missing) > 0 || len(c.corrupt) > 0 {
290		var action string
291		if c.allowMissing {
292			action = "missing objects"
293		} else {
294			action = "failed"
295		}
296
297		Print("LFS upload %s:", action)
298		for name, oid := range c.missing {
299			Print("  (missing) %s (%s)", name, oid)
300		}
301		for name, oid := range c.corrupt {
302			Print("  (corrupt) %s (%s)", name, oid)
303		}
304
305		if !c.allowMissing {
306			pushMissingHint := []string{
307				"hint: Your push was rejected due to missing or corrupt local objects.",
308				"hint: You can disable this check with: 'git config lfs.allowincompletepush true'",
309			}
310			Print(strings.Join(pushMissingHint, "\n"))
311			os.Exit(2)
312		}
313	}
314
315	if len(c.otherErrs) > 0 {
316		os.Exit(2)
317	}
318
319	if c.lockVerifier.HasUnownedLocks() {
320		Print("Unable to push locked files:")
321		for _, unowned := range c.lockVerifier.UnownedLocks() {
322			Print("* %s - %s", unowned.Path(), unowned.Owners())
323		}
324
325		if c.lockVerifier.Enabled() {
326			Exit("ERROR: Cannot update locked files.")
327		} else {
328			Error("WARNING: The above files would have halted this push.")
329		}
330	} else if c.lockVerifier.HasOwnedLocks() {
331		Print("Consider unlocking your own locked files: (`git lfs unlock <path>`)")
332		for _, owned := range c.lockVerifier.OwnedLocks() {
333			Print("* %s", owned.Path())
334		}
335	}
336}
337
338var (
339	githubHttps, _ = url.Parse("https://github.com")
340	githubSsh, _   = url.Parse("ssh://github.com")
341
342	// hostsWithKnownLockingSupport is a list of scheme-less hostnames
343	// (without port numbers) that are known to implement the LFS locking
344	// API.
345	//
346	// Additions are welcome.
347	hostsWithKnownLockingSupport = []*url.URL{
348		githubHttps, githubSsh,
349	}
350)
351
352func (c *uploadContext) uploadTransfer(p *lfs.WrappedPointer) (*tq.Transfer, error) {
353	var missing bool
354
355	filename := p.Name
356	oid := p.Oid
357
358	localMediaPath, err := c.gitfilter.ObjectPath(oid)
359	if err != nil {
360		return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid)
361	}
362
363	if len(filename) > 0 {
364		if missing, err = c.ensureFile(filename, localMediaPath, oid); err != nil && !errors.IsCleanPointerError(err) {
365			return nil, err
366		}
367	}
368
369	return &tq.Transfer{
370		Name:    filename,
371		Path:    localMediaPath,
372		Oid:     oid,
373		Size:    p.Size,
374		Missing: missing,
375	}, nil
376}
377
378// ensureFile makes sure that the cleanPath exists before pushing it.  If it
379// does not exist, it attempts to clean it by reading the file at smudgePath.
380func (c *uploadContext) ensureFile(smudgePath, cleanPath, oid string) (bool, error) {
381	if _, err := os.Stat(cleanPath); err == nil {
382		return false, nil
383	}
384
385	localPath := filepath.Join(cfg.LocalWorkingDir(), smudgePath)
386	file, err := os.Open(localPath)
387	if err != nil {
388		return !c.allowMissing, nil
389	}
390
391	defer file.Close()
392
393	stat, err := file.Stat()
394	if err != nil {
395		return false, err
396	}
397
398	cleaned, err := c.gitfilter.Clean(file, file.Name(), stat.Size(), nil)
399	if cleaned != nil {
400		cleaned.Teardown()
401	}
402
403	if err != nil {
404		return false, err
405	}
406	return false, nil
407}
408
409// supportsLockingAPI returns whether or not a given url is known to support
410// the LFS locking API by whether or not its hostname is included in the list
411// above.
412func supportsLockingAPI(rawurl string) bool {
413	u, err := url.Parse(rawurl)
414	if err != nil {
415		tracerx.Printf("commands: unable to parse %q to determine locking support: %v", rawurl, err)
416		return false
417	}
418
419	for _, supported := range hostsWithKnownLockingSupport {
420		if supported.Scheme == u.Scheme &&
421			supported.Hostname() == u.Hostname() &&
422			strings.HasPrefix(u.Path, supported.Path) {
423			return true
424		}
425	}
426	return false
427}
428
429// disableFor disables lock verification for the given lfsapi.Endpoint,
430// "endpoint".
431func disableFor(rawurl string) error {
432	tracerx.Printf("commands: disabling lock verification for %q", rawurl)
433
434	key := strings.Join([]string{"lfs", rawurl, "locksverify"}, ".")
435
436	_, err := cfg.SetGitLocalKey(key, "false")
437	return err
438}
439