github.com/hawser/git-hawser@v2.5.2+incompatible/commands/command_migrate.go (about)

     1  package commands
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/git-lfs/git-lfs/errors"
    11  	"github.com/git-lfs/git-lfs/git"
    12  	"github.com/git-lfs/git-lfs/git/githistory"
    13  	"github.com/git-lfs/git-lfs/tasklog"
    14  	"github.com/git-lfs/gitobj"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  var (
    19  	// migrateIncludeRefs is a set of Git references to explicitly include
    20  	// in the migration.
    21  	migrateIncludeRefs []string
    22  	// migrateExcludeRefs is a set of Git references to explicitly exclude
    23  	// in the migration.
    24  	migrateExcludeRefs []string
    25  
    26  	// migrateYes indicates that an answer of 'yes' should be presumed
    27  	// whenever 'git lfs migrate' asks for user input.
    28  	migrateYes bool
    29  
    30  	// migrateSkipFetch assumes that the client has the latest copy of
    31  	// remote references, and thus should not contact the remote for a set
    32  	// of updated references.
    33  	migrateSkipFetch bool
    34  
    35  	// migrateEverything indicates the presence of the --everything flag,
    36  	// and instructs 'git lfs migrate' to migrate all local references.
    37  	migrateEverything bool
    38  
    39  	// migrateVerbose enables verbose logging
    40  	migrateVerbose bool
    41  
    42  	// objectMapFile is the path to the map of old sha1 to new sha1
    43  	// commits
    44  	objectMapFilePath string
    45  
    46  	// migrateNoRewrite is the flag indicating whether or not the
    47  	// command should rewrite git history
    48  	migrateNoRewrite bool
    49  	// migrateCommitMessage is the message to use with the commit generated
    50  	// by the migrate command
    51  	migrateCommitMessage string
    52  
    53  	// exportRemote is the remote from which to download objects when
    54  	// performing an export
    55  	exportRemote string
    56  
    57  	// migrateFixup is the flag indicating whether or not to infer the
    58  	// included and excluded filepath patterns.
    59  	migrateFixup bool
    60  )
    61  
    62  // migrate takes the given command and arguments, *gitobj.ObjectDatabase, as well
    63  // as a BlobRewriteFn to apply, and performs a migration.
    64  func migrate(args []string, r *githistory.Rewriter, l *tasklog.Logger, opts *githistory.RewriteOptions) {
    65  	requireInRepo()
    66  
    67  	opts, err := rewriteOptions(args, opts, l)
    68  	if err != nil {
    69  		ExitWithError(err)
    70  	}
    71  
    72  	_, err = r.Rewrite(opts)
    73  	if err != nil {
    74  		ExitWithError(err)
    75  	}
    76  }
    77  
    78  // getObjectDatabase creates a *git.ObjectDatabase from the filesystem pointed
    79  // at the .git directory of the currently checked-out repository.
    80  func getObjectDatabase() (*gitobj.ObjectDatabase, error) {
    81  	dir, err := git.GitDir()
    82  	if err != nil {
    83  		return nil, errors.Wrap(err, "cannot open root")
    84  	}
    85  	return gitobj.FromFilesystem(filepath.Join(dir, "objects"), cfg.TempDir())
    86  }
    87  
    88  // rewriteOptions returns *githistory.RewriteOptions able to be passed to a
    89  // *githistory.Rewriter that reflect the current arguments and flags passed to
    90  // an invocation of git-lfs-migrate(1).
    91  //
    92  // It is merged with the given "opts". In other words, an identical "opts" is
    93  // returned, where the Include and Exclude fields have been filled based on the
    94  // following rules:
    95  //
    96  // The included and excluded references are determined based on the output of
    97  // includeExcludeRefs (see below for documentation and detail).
    98  //
    99  // If any of the above could not be determined without error, that error will be
   100  // returned immediately.
   101  func rewriteOptions(args []string, opts *githistory.RewriteOptions, l *tasklog.Logger) (*githistory.RewriteOptions, error) {
   102  	include, exclude, err := includeExcludeRefs(l, args)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return &githistory.RewriteOptions{
   108  		Include: include,
   109  		Exclude: exclude,
   110  
   111  		UpdateRefs:        opts.UpdateRefs,
   112  		Verbose:           opts.Verbose,
   113  		ObjectMapFilePath: opts.ObjectMapFilePath,
   114  
   115  		BlobFn:            opts.BlobFn,
   116  		TreePreCallbackFn: opts.TreePreCallbackFn,
   117  		TreeCallbackFn:    opts.TreeCallbackFn,
   118  	}, nil
   119  }
   120  
   121  // includeExcludeRefs returns fully-qualified sets of references to include, and
   122  // exclude, or an error if those could not be determined.
   123  //
   124  // They are determined based on the following rules:
   125  //
   126  //   - Include all local refs/heads/<branch> references for each branch
   127  //     specified as an argument.
   128  //   - Include the currently checked out branch if no branches are given as
   129  //     arguments and the --include-ref= or --exclude-ref= flag(s) aren't given.
   130  //   - Include all references given in --include-ref=<ref>.
   131  //   - Exclude all references given in --exclude-ref=<ref>.
   132  func includeExcludeRefs(l *tasklog.Logger, args []string) (include, exclude []string, err error) {
   133  	hardcore := len(migrateIncludeRefs) > 0 || len(migrateExcludeRefs) > 0
   134  
   135  	if len(args) == 0 && !hardcore && !migrateEverything {
   136  		// If no branches were given explicitly AND neither
   137  		// --include-ref or --exclude-ref flags were given, then add the
   138  		// currently checked out reference.
   139  		current, err := currentRefToMigrate()
   140  		if err != nil {
   141  			return nil, nil, err
   142  		}
   143  		args = append(args, current.Name)
   144  	}
   145  
   146  	if migrateEverything && len(args) > 0 {
   147  		return nil, nil, errors.New("fatal: cannot use --everything with explicit reference arguments")
   148  	}
   149  
   150  	for _, name := range args {
   151  		var excluded bool
   152  		if strings.HasPrefix("^", name) {
   153  			name = name[1:]
   154  			excluded = true
   155  		}
   156  
   157  		// Then, loop through each branch given, resolve that reference,
   158  		// and include it.
   159  		ref, err := git.ResolveRef(name)
   160  		if err != nil {
   161  			return nil, nil, err
   162  		}
   163  
   164  		if excluded {
   165  			exclude = append(exclude, ref.Refspec())
   166  		} else {
   167  			include = append(include, ref.Refspec())
   168  		}
   169  	}
   170  
   171  	if hardcore {
   172  		if migrateEverything {
   173  			return nil, nil, errors.New("fatal: cannot use --everything with --include-ref or --exclude-ref")
   174  		}
   175  
   176  		// If either --include-ref=<ref> or --exclude-ref=<ref> were
   177  		// given, append those to the include and excluded reference
   178  		// set, respectively.
   179  		include = append(include, migrateIncludeRefs...)
   180  		exclude = append(exclude, migrateExcludeRefs...)
   181  	} else if migrateEverything {
   182  		refs, err := git.AllRefsIn("")
   183  		if err != nil {
   184  			return nil, nil, err
   185  		}
   186  
   187  		for _, ref := range refs {
   188  			switch ref.Type {
   189  			case git.RefTypeLocalBranch, git.RefTypeLocalTag,
   190  				git.RefTypeRemoteBranch, git.RefTypeRemoteTag:
   191  
   192  				include = append(include, ref.Refspec())
   193  			case git.RefTypeOther:
   194  				parts := strings.SplitN(ref.Refspec(), "/", 3)
   195  				if len(parts) < 2 {
   196  					continue
   197  				}
   198  
   199  				switch parts[1] {
   200  				// The following are GitLab-, GitHub-, VSTS-,
   201  				// and BitBucket-specific reference naming
   202  				// conventions.
   203  				case "merge-requests", "pull", "pull-requests":
   204  					include = append(include, ref.Refspec())
   205  				}
   206  			}
   207  		}
   208  	} else {
   209  		bare, err := git.IsBare()
   210  		if err != nil {
   211  			return nil, nil, errors.Wrap(err, "fatal: unable to determine bareness")
   212  		}
   213  
   214  		if !bare {
   215  			// Otherwise, if neither --include-ref=<ref> or
   216  			// --exclude-ref=<ref> were given, include no additional
   217  			// references, and exclude all remote references that
   218  			// are remote branches or remote tags.
   219  			remoteRefs, err := getRemoteRefs(l)
   220  			if err != nil {
   221  				return nil, nil, err
   222  			}
   223  
   224  			for _, rr := range remoteRefs {
   225  				exclude = append(exclude, rr.Refspec())
   226  			}
   227  		}
   228  	}
   229  
   230  	return include, exclude, nil
   231  }
   232  
   233  // getRemoteRefs returns a fully qualified set of references belonging to all
   234  // remotes known by the currently checked-out repository, or an error if those
   235  // references could not be determined.
   236  func getRemoteRefs(l *tasklog.Logger) ([]*git.Ref, error) {
   237  	var refs []*git.Ref
   238  
   239  	remotes, err := git.RemoteList()
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	if !migrateSkipFetch {
   245  		w := l.Waiter("migrate: Fetching remote refs")
   246  		if err := git.Fetch(remotes...); err != nil {
   247  			return nil, err
   248  		}
   249  		w.Complete()
   250  	}
   251  
   252  	for _, remote := range remotes {
   253  		var refsForRemote []*git.Ref
   254  		if migrateSkipFetch {
   255  			refsForRemote, err = git.CachedRemoteRefs(remote)
   256  		} else {
   257  			refsForRemote, err = git.RemoteRefs(remote)
   258  		}
   259  
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  
   264  		for _, rr := range refsForRemote {
   265  			// HACK(@ttaylorr): add remote name to fully-qualify
   266  			// references:
   267  			rr.Name = fmt.Sprintf("%s/%s", remote, rr.Name)
   268  
   269  			refs = append(refs, rr)
   270  		}
   271  	}
   272  
   273  	return refs, nil
   274  }
   275  
   276  // formatRefName returns the fully-qualified name for the given Git reference
   277  // "ref".
   278  func formatRefName(ref *git.Ref, remote string) string {
   279  	var name []string
   280  
   281  	switch ref.Type {
   282  	case git.RefTypeRemoteBranch:
   283  		name = []string{"refs", "remotes", remote, ref.Name}
   284  	case git.RefTypeRemoteTag:
   285  		name = []string{"refs", "tags", ref.Name}
   286  	default:
   287  		return ref.Name
   288  	}
   289  	return strings.Join(name, "/")
   290  
   291  }
   292  
   293  // currentRefToMigrate returns the fully-qualified name of the currently
   294  // checked-out reference, or an error if the reference's type was not a local
   295  // branch.
   296  func currentRefToMigrate() (*git.Ref, error) {
   297  	current, err := git.CurrentRef()
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	if current.Type == git.RefTypeOther ||
   303  		current.Type == git.RefTypeRemoteBranch ||
   304  		current.Type == git.RefTypeRemoteTag {
   305  
   306  		return nil, errors.Errorf("fatal: cannot migrate non-local ref: %s", current.Name)
   307  	}
   308  	return current, nil
   309  }
   310  
   311  // getHistoryRewriter returns a history rewriter that includes the filepath
   312  // filter given by the --include and --exclude arguments.
   313  func getHistoryRewriter(cmd *cobra.Command, db *gitobj.ObjectDatabase, l *tasklog.Logger) *githistory.Rewriter {
   314  	include, exclude := getIncludeExcludeArgs(cmd)
   315  	filter := buildFilepathFilter(cfg, include, exclude)
   316  
   317  	return githistory.NewRewriter(db,
   318  		githistory.WithFilter(filter), githistory.WithLogger(l))
   319  }
   320  
   321  func ensureWorkingCopyClean(in io.Reader, out io.Writer) {
   322  	dirty, err := git.IsWorkingCopyDirty()
   323  	if err != nil {
   324  		ExitWithError(errors.Wrap(err,
   325  			"fatal: could not determine if working copy is dirty"))
   326  	}
   327  
   328  	if !dirty {
   329  		return
   330  	}
   331  
   332  	var proceed bool
   333  	if migrateYes {
   334  		proceed = true
   335  	} else {
   336  		answer := bufio.NewReader(in)
   337  	L:
   338  		for {
   339  			fmt.Fprintf(out, "migrate: override changes in your working copy? [Y/n] ")
   340  			s, err := answer.ReadString('\n')
   341  			if err != nil {
   342  				if err == io.EOF {
   343  					break L
   344  				}
   345  				ExitWithError(errors.Wrap(err,
   346  					"fatal: could not read answer"))
   347  			}
   348  
   349  			switch strings.TrimSpace(s) {
   350  			case "n", "N":
   351  				proceed = false
   352  				break L
   353  			case "y", "Y":
   354  				proceed = true
   355  				break L
   356  			}
   357  
   358  			if !strings.HasSuffix(s, "\n") {
   359  				fmt.Fprintf(out, "\n")
   360  			}
   361  		}
   362  	}
   363  
   364  	if proceed {
   365  		fmt.Fprintf(out, "migrate: changes in your working copy will be overridden ...\n")
   366  	} else {
   367  		Exit("migrate: working copy must not be dirty")
   368  	}
   369  }
   370  
   371  func init() {
   372  	info := NewCommand("info", migrateInfoCommand)
   373  	info.Flags().IntVar(&migrateInfoTopN, "top", 5, "--top=<n>")
   374  	info.Flags().StringVar(&migrateInfoAboveFmt, "above", "", "--above=<n>")
   375  	info.Flags().StringVar(&migrateInfoUnitFmt, "unit", "", "--unit=<unit>")
   376  
   377  	importCmd := NewCommand("import", migrateImportCommand)
   378  	importCmd.Flags().BoolVar(&migrateVerbose, "verbose", false, "Verbose logging")
   379  	importCmd.Flags().StringVar(&objectMapFilePath, "object-map", "", "Object map file")
   380  	importCmd.Flags().BoolVar(&migrateNoRewrite, "no-rewrite", false, "Add new history without rewriting previous")
   381  	importCmd.Flags().StringVarP(&migrateCommitMessage, "message", "m", "", "With --no-rewrite, an optional commit message")
   382  	importCmd.Flags().BoolVar(&migrateFixup, "fixup", false, "Infer filepaths based on .gitattributes")
   383  
   384  	exportCmd := NewCommand("export", migrateExportCommand)
   385  	exportCmd.Flags().BoolVar(&migrateVerbose, "verbose", false, "Verbose logging")
   386  	exportCmd.Flags().StringVar(&objectMapFilePath, "object-map", "", "Object map file")
   387  	exportCmd.Flags().StringVar(&exportRemote, "remote", "", "Remote from which to download objects")
   388  
   389  	RegisterCommand("migrate", nil, func(cmd *cobra.Command) {
   390  		cmd.PersistentFlags().StringVarP(&includeArg, "include", "I", "", "Include a list of paths")
   391  		cmd.PersistentFlags().StringVarP(&excludeArg, "exclude", "X", "", "Exclude a list of paths")
   392  
   393  		cmd.PersistentFlags().StringSliceVar(&migrateIncludeRefs, "include-ref", nil, "An explicit list of refs to include")
   394  		cmd.PersistentFlags().StringSliceVar(&migrateExcludeRefs, "exclude-ref", nil, "An explicit list of refs to exclude")
   395  		cmd.PersistentFlags().BoolVar(&migrateEverything, "everything", false, "Migrate all local references")
   396  		cmd.PersistentFlags().BoolVar(&migrateSkipFetch, "skip-fetch", false, "Assume up-to-date remote references.")
   397  
   398  		cmd.PersistentFlags().BoolVarP(&migrateYes, "yes", "y", false, "Don't prompt for answers.")
   399  
   400  		cmd.AddCommand(exportCmd, importCmd, info)
   401  	})
   402  }