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 }