2 {-# LANGUAGE LambdaCase #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE RankNTypes #-}
5 {-# LANGUAGE RecordWildCards #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
8 module Distribution
.Client
.VCS
19 -- * Validating 'SourceRepo's and configuring VCS drivers
20 , validatePDSourceRepo
23 , SourceRepoProblem
(..)
27 -- * Running the VCS driver
31 -- * The individual VCS drivers
41 import Distribution
.Client
.Compat
.Prelude
44 import Distribution
.Client
.RebuildMonad
47 , monitorDirectoryExistence
50 import Distribution
.Client
.Types
.SourceRepo
(SourceRepoMaybe
, SourceRepositoryPackage
(..), srpToProxy
)
51 import qualified Distribution
.PackageDescription
as PD
52 import Distribution
.Simple
.Program
53 ( ConfiguredProgram
(programVersion
)
54 , Program
(programFindVersion
)
55 , ProgramInvocation
(..)
58 , getProgramInvocationOutput
61 , runProgramInvocation
64 import Distribution
.Types
.SourceRepo
68 import Distribution
.Verbosity
as Verbosity
71 import Distribution
.Version
75 #if !MIN_VERSION_base
(4,18,0)
76 import Control
.Applicative
80 import Control
.Exception
84 import Control
.Monad
.Trans
87 import qualified Data
.Char as Char
88 import qualified Data
.List
as List
89 import qualified Data
.Map
as Map
90 import System
.Directory
92 , removeDirectoryRecursive
94 import System
.FilePath
98 import System
.IO.Error
102 -- | A driver for a version control system, e.g. git, darcs etc.
103 data VCS program
= VCS
104 { vcsRepoType
:: RepoType
105 -- ^ The type of repository this driver is for.
106 , vcsProgram
:: program
107 -- ^ The vcs program itself.
108 -- This is used at type 'Program' and 'ConfiguredProgram'.
113 -> SourceRepositoryPackage f
114 -> FilePath -- Source URI
115 -> FilePath -- Destination directory
116 -> [ProgramInvocation
]
117 -- ^ The program invocation(s) to get\/clone a repository into a fresh
123 -> [(SourceRepositoryPackage f
, FilePath)]
124 -> IO [MonitorFilePath
]
125 -- ^ The program invocation(s) to synchronise a whole set of /related/
126 -- repositories with corresponding local directories. Also returns the
127 -- files that the command depends on, for change monitoring.
130 -- ------------------------------------------------------------
132 -- * Selecting repos and drivers
134 -- ------------------------------------------------------------
136 data SourceRepoProblem
137 = SourceRepoRepoTypeUnspecified
138 | SourceRepoRepoTypeUnsupported
(SourceRepositoryPackage Proxy
) RepoType
139 | SourceRepoLocationUnspecified
142 -- | Validates that the 'SourceRepo' specifies a location URI and a repository
143 -- type that is supported by a VCS driver.
145 -- | It also returns the 'VCS' driver we should use to work with it.
147 :: SourceRepositoryPackage f
148 -> Either SourceRepoProblem
(SourceRepositoryPackage f
, String, RepoType
, VCS Program
)
149 validateSourceRepo
= \repo
-> do
150 let rtype
= srpType repo
151 vcs
<- Map
.lookup rtype knownVCSs ?
! SourceRepoRepoTypeUnsupported
(srpToProxy repo
) rtype
152 let uri
= srpLocation repo
153 return (repo
, uri
, rtype
, vcs
)
155 a ?
! e
= maybe (Left e
) Right a
159 -> Either SourceRepoProblem
(SourceRepoMaybe
, String, RepoType
, VCS Program
)
160 validatePDSourceRepo repo
= do
161 rtype
<- PD
.repoType repo ?
! SourceRepoRepoTypeUnspecified
162 uri
<- PD
.repoLocation repo ?
! SourceRepoLocationUnspecified
164 SourceRepositoryPackage
167 , srpTag
= PD
.repoTag repo
168 , srpBranch
= PD
.repoBranch repo
169 , srpSubdir
= PD
.repoSubdir repo
170 , srpCommand
= mempty
173 a ?
! e
= maybe (Left e
) Right a
175 -- | As 'validateSourceRepo' but for a bunch of 'SourceRepo's, and return
176 -- things in a convenient form to pass to 'configureVCSs', or to report
179 :: [SourceRepositoryPackage f
]
181 [(SourceRepositoryPackage f
, SourceRepoProblem
)]
182 [(SourceRepositoryPackage f
, String, RepoType
, VCS Program
)]
183 validateSourceRepos rs
=
184 case partitionEithers
(map validateSourceRepo
' rs
) of
185 (problems
@(_
: _
), _
) -> Left problems
186 ([], vcss
) -> Right vcss
189 :: SourceRepositoryPackage f
191 (SourceRepositoryPackage f
, SourceRepoProblem
)
192 (SourceRepositoryPackage f
, String, RepoType
, VCS Program
)
193 validateSourceRepo
' r
=
197 (validateSourceRepo r
)
202 -> IO (VCS ConfiguredProgram
)
203 configureVCS verbosity vcs
@VCS
{vcsProgram
= prog
} =
204 asVcsConfigured
<$> requireProgram verbosity prog emptyProgramDb
206 asVcsConfigured
(prog
', _
) = vcs
{vcsProgram
= prog
'}
210 -> Map RepoType
(VCS Program
)
211 -> IO (Map RepoType
(VCS ConfiguredProgram
))
212 configureVCSs verbosity
= traverse
(configureVCS verbosity
)
214 -- ------------------------------------------------------------
216 -- * Running the driver
218 -- ------------------------------------------------------------
220 -- | Clone a single source repo into a fresh directory, using a configured VCS.
222 -- This is for making a new copy, not synchronising an existing copy. It will
223 -- fail if the destination directory already exists.
225 -- Make sure to validate the 'SourceRepo' using 'validateSourceRepo' first.
228 -> VCS ConfiguredProgram
229 -> SourceRepositoryPackage f
235 repo
@SourceRepositoryPackage
{srpLocation
= srcuri
}
237 traverse_
(runProgramInvocation verbosity
) invocations
248 -- | Synchronise a set of 'SourceRepo's referring to the same repository with
249 -- corresponding local directories. The local directories may or may not
252 -- The 'SourceRepo' values used in a single invocation of 'syncSourceRepos',
253 -- or used across a series of invocations with any local directory must refer
254 -- to the /same/ repository. That means it must be the same location but they
255 -- can differ in the branch, or tag or subdir.
257 -- The reason to allow multiple related 'SourceRepo's is to allow for the
258 -- network or storage to be shared between different checkouts of the repo.
259 -- For example if a single repo contains multiple packages in different subdirs
260 -- and in some project it may make sense to use a different state of the repo
261 -- for one subdir compared to another.
264 -> VCS ConfiguredProgram
265 -> [(SourceRepositoryPackage f
, FilePath)]
267 syncSourceRepos verbosity vcs repos
= do
268 files
<- liftIO
$ vcsSyncRepos vcs verbosity
(vcsProgram vcs
) repos
271 -- ------------------------------------------------------------
273 -- * The various VCS drivers
275 -- ------------------------------------------------------------
277 -- | The set of all supported VCS drivers, organised by 'RepoType'.
278 knownVCSs
:: Map RepoType
(VCS Program
)
279 knownVCSs
= Map
.fromList
[(vcsRepoType vcs
, vcs
) | vcs
<- vcss
]
281 vcss
= [vcsBzr
, vcsDarcs
, vcsGit
, vcsHg
, vcsSvn
]
283 -- | VCS driver for Bazaar.
284 vcsBzr
:: VCS Program
287 { vcsRepoType
= KnownRepoType Bazaar
288 , vcsProgram
= bzrProgram
296 -> SourceRepositoryPackage f
299 -> [ProgramInvocation
]
300 vcsCloneRepo verbosity prog repo srcuri destdir
=
303 ([branchCmd
, srcuri
, destdir
] ++ tagArgs
++ verboseArg
)
306 -- The @get@ command was deprecated in version 2.4 in favour of
307 -- the alias @branch@
309 | programVersion prog
>= Just
(mkVersion
[2, 4]) =
314 tagArgs
= case srpTag repo
of
316 Just tag
-> ["-r", "tag:" ++ tag
]
317 verboseArg
:: [String]
318 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
323 -> [(SourceRepositoryPackage f
, FilePath)]
324 -> IO [MonitorFilePath
]
325 vcsSyncRepos _v _p _rs
= fail "sync repo not yet supported for bzr"
327 bzrProgram
:: Program
329 (simpleProgram
"bzr")
330 { programFindVersion
= findProgramVersion
"--version" $ \str
->
332 -- "Bazaar (bzr) 2.6.0\n ... lots of extra stuff"
333 (_
: _
: ver
: _
) -> ver
337 -- | VCS driver for Darcs.
338 vcsDarcs
:: VCS Program
341 { vcsRepoType
= KnownRepoType Darcs
342 , vcsProgram
= darcsProgram
350 -> SourceRepositoryPackage f
353 -> [ProgramInvocation
]
354 vcsCloneRepo verbosity prog repo srcuri destdir
=
355 [programInvocation prog cloneArgs
]
357 cloneArgs
:: [String]
358 cloneArgs
= [cloneCmd
, srcuri
, destdir
] ++ tagArgs
++ verboseArg
359 -- At some point the @clone@ command was introduced as an alias for
360 -- @get@, and @clone@ seems to be the recommended one now.
363 | programVersion prog
>= Just
(mkVersion
[2, 8]) =
367 tagArgs
= case srpTag repo
of
369 Just tag
-> ["-t", tag
]
370 verboseArg
:: [String]
371 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
376 -> [(SourceRepositoryPackage f
, FilePath)]
377 -> IO [MonitorFilePath
]
378 vcsSyncRepos _ _
[] = return []
379 vcsSyncRepos verbosity prog
((primaryRepo
, primaryLocalDir
) : secondaryRepos
) =
381 vcsSyncRepo verbosity prog primaryRepo primaryLocalDir Nothing
382 for_ secondaryRepos
$ \(repo
, localDir
) ->
383 vcsSyncRepo verbosity prog repo localDir
$ Just primaryLocalDir
386 dirs
= primaryLocalDir
: (snd <$> secondaryRepos
)
387 monitors
:: [MonitorFilePath
]
388 monitors
= monitorDirectoryExistence
<$> dirs
390 vcsSyncRepo verbosity prog SourceRepositoryPackage
{..} localDir _peer
=
391 try (lines <$> darcsWithOutput localDir
["log", "--last", "1"]) >>= \case
392 Right
(_
: _
: _
: x
: _
)
393 | Just tag
<- (List
.stripPrefix
"tagged " . List
.dropWhile Char.isSpace) x
394 , Just tag
' <- srpTag
397 Left e |
not (isDoesNotExistError e
) -> throw e
399 removeDirectoryRecursive localDir `
catch` liftA2
unless isDoesNotExistError throw
400 darcs
(takeDirectory localDir
) cloneArgs
402 darcs
:: FilePath -> [String] -> IO ()
403 darcs
= darcs
' runProgramInvocation
405 darcsWithOutput
:: FilePath -> [String] -> IO String
406 darcsWithOutput
= darcs
' getProgramInvocationOutput
408 darcs
' :: (Verbosity
-> ProgramInvocation
-> t
) -> FilePath -> [String] -> t
412 (programInvocation prog args
)
413 { progInvokeCwd
= Just cwd
416 cloneArgs
:: [String]
417 cloneArgs
= ["clone"] ++ tagArgs
++ [srpLocation
, localDir
] ++ verboseArg
419 tagArgs
= case srpTag
of
421 Just tag
-> ["-t" ++ tag
]
422 verboseArg
:: [String]
423 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
425 darcsProgram
:: Program
427 (simpleProgram
"darcs")
428 { programFindVersion
= findProgramVersion
"--version" $ \str
->
435 -- | VCS driver for Git.
436 vcsGit
:: VCS Program
439 { vcsRepoType
= KnownRepoType Git
440 , vcsProgram
= gitProgram
448 -> SourceRepositoryPackage f
451 -> [ProgramInvocation
]
452 vcsCloneRepo verbosity prog repo srcuri destdir
=
453 [programInvocation prog cloneArgs
]
454 -- And if there's a tag, we have to do that in a second step:
455 ++ [git
(resetArgs tag
) | tag
<- maybeToList (srpTag repo
)]
456 ++ [ git
(["submodule", "sync", "--recursive"] ++ verboseArg
)
457 , git
(["submodule", "update", "--init", "--force", "--recursive"] ++ verboseArg
)
460 git args
= (programInvocation prog args
){progInvokeCwd
= Just destdir
}
462 ["clone", srcuri
, destdir
]
465 branchArgs
= case srpBranch repo
of
466 Just b
-> ["--branch", b
]
468 resetArgs tag
= "reset" : verboseArg
++ ["--hard", tag
, "--"]
469 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
474 -> [(SourceRepositoryPackage f
, FilePath)]
475 -> IO [MonitorFilePath
]
476 vcsSyncRepos _ _
[] = return []
480 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
481 vcsSyncRepo verbosity gitProg primaryRepo primaryLocalDir Nothing
483 [ vcsSyncRepo verbosity gitProg repo localDir
(Just primaryLocalDir
)
484 |
(repo
, localDir
) <- secondaryRepos
487 [ monitorDirectoryExistence dir
488 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
491 vcsSyncRepo verbosity gitProg SourceRepositoryPackage
{..} localDir peer
= do
492 exists
<- doesDirectoryExist localDir
494 then git localDir
["fetch"]
495 else git
(takeDirectory localDir
) cloneArgs
496 -- Before trying to checkout other commits, all submodules must be
497 -- de-initialised and the .git/modules directory must be deleted. This
498 -- is needed because sometimes `git submodule sync` does not actually
499 -- update the submodule source URL. Detailed description here:
500 -- https://git.coop/-/snippets/85
501 git localDir
["submodule", "deinit", "--force", "--all"]
502 let gitModulesDir
= localDir
</> ".git" </> "modules"
503 gitModulesExists
<- doesDirectoryExist gitModulesDir
504 when gitModulesExists
$ removeDirectoryRecursive gitModulesDir
505 git localDir resetArgs
506 git localDir
$ ["submodule", "sync", "--recursive"] ++ verboseArg
507 git localDir
$ ["submodule", "update", "--force", "--init", "--recursive"] ++ verboseArg
508 git localDir
$ ["submodule", "foreach", "--recursive"] ++ verboseArg
++ ["git clean -ffxdq"]
509 git localDir
$ ["clean", "-ffxdq"]
511 git
:: FilePath -> [String] -> IO ()
513 runProgramInvocation verbosity
$
514 (programInvocation gitProg args
)
515 { progInvokeCwd
= Just cwd
519 ["clone", "--no-checkout", loc
, localDir
]
522 Just peerLocalDir
-> ["--reference", peerLocalDir
]
526 resetArgs
= "reset" : verboseArg
++ ["--hard", resetTarget
, "--"]
527 resetTarget
= fromMaybe "HEAD" (srpBranch `mplus` srpTag
)
528 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
530 gitProgram
:: Program
532 (simpleProgram
"git")
533 { programFindVersion
= findProgramVersion
"--version" $ \str
->
535 -- "git version 2.5.5"
536 (_
: _
: ver
: _
) |
all isTypical ver
-> ver
537 -- or annoyingly "git version 2.17.1.windows.2" yes, really
540 . takeWhile (all isNum
)
546 isNum c
= c
>= '0' && c
<= '9'
547 isTypical c
= isNum c || c
== '.'
548 split cs
= case break (== '.') cs
of
549 (chunk
, []) -> chunk
: []
550 (chunk
, _
: rest
) -> chunk
: split rest
552 -- | VCS driver for Mercurial.
556 { vcsRepoType
= KnownRepoType Mercurial
557 , vcsProgram
= hgProgram
565 -> SourceRepositoryPackage f
568 -> [ProgramInvocation
]
569 vcsCloneRepo verbosity prog repo srcuri destdir
=
570 [programInvocation prog cloneArgs
]
573 ["clone", srcuri
, destdir
]
577 branchArgs
= case srpBranch repo
of
578 Just b
-> ["--branch", b
]
580 tagArgs
= case srpTag repo
of
581 Just t
-> ["--rev", t
]
583 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
588 -> [(SourceRepositoryPackage f
, FilePath)]
589 -> IO [MonitorFilePath
]
590 vcsSyncRepos _ _
[] = return []
594 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
595 vcsSyncRepo verbosity hgProg primaryRepo primaryLocalDir
597 [ vcsSyncRepo verbosity hgProg repo localDir
598 |
(repo
, localDir
) <- secondaryRepos
601 [ monitorDirectoryExistence dir
602 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
604 vcsSyncRepo verbosity hgProg repo localDir
= do
605 exists
<- doesDirectoryExist localDir
607 then hg localDir
["pull"]
608 else hg
(takeDirectory localDir
) cloneArgs
609 hg localDir checkoutArgs
611 hg
:: FilePath -> [String] -> IO ()
613 runProgramInvocation verbosity
$
614 (programInvocation hgProg args
)
615 { progInvokeCwd
= Just cwd
618 ["clone", "--noupdate", (srpLocation repo
), localDir
]
620 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
622 ["checkout", "--clean"]
624 tagArgs
= case srpTag repo
of
625 Just t
-> ["--rev", t
]
631 { programFindVersion
= findProgramVersion
"--version" $ \str
->
633 -- Mercurial Distributed SCM (version 3.5.2)\n ... long message
634 (_
: _
: _
: _
: ver
: _
) -> takeWhile (\c
-> Char.isDigit c || c
== '.') ver
638 -- | VCS driver for Subversion.
639 vcsSvn
:: VCS Program
642 { vcsRepoType
= KnownRepoType SVN
643 , vcsProgram
= svnProgram
651 -> SourceRepositoryPackage f
654 -> [ProgramInvocation
]
655 vcsCloneRepo verbosity prog _repo srcuri destdir
=
656 [programInvocation prog checkoutArgs
]
658 checkoutArgs
= ["checkout", srcuri
, destdir
] ++ verboseArg
659 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
660 -- TODO: branch or tag?
665 -> [(SourceRepositoryPackage f
, FilePath)]
666 -> IO [MonitorFilePath
]
667 vcsSyncRepos _v _p _rs
= fail "sync repo not yet supported for svn"
669 svnProgram
:: Program
671 (simpleProgram
"svn")
672 { programFindVersion
= findProgramVersion
"--version" $ \str
->
674 -- svn, version 1.9.4 (r1740329)\n ... long message
675 (_
: _
: ver
: _
) -> ver
679 -- | VCS driver for Pijul.
680 -- Documentation for Pijul can be found at <https://pijul.org/manual/introduction.html>
684 -- As far as I understand pijul, there are branches and "tags" in pijul,
685 -- but there aren't a "commit hash" identifying an arbitrary state.
687 -- One can create `a pijul tag`, which will make a patch hash,
688 -- which depends on everything currently in the repository.
689 -- I guess if you try to apply that patch, you'll be forced to apply
690 -- all the dependencies too. In other words, there are no named tags.
692 -- It's not clear to me whether there is an option to
693 -- "apply this patch *and* all of its dependencies".
694 -- And relatedly, whether how to make sure that there are no other
697 -- With branches it's easier, as you can `pull` and `checkout` them,
698 -- and they seem to be similar enough. Yet, pijul documentations says
700 -- > Note that the purpose of branches in Pijul is quite different from Git,
701 -- since Git's "feature branches" can usually be implemented by just
704 -- I guess it means that indeed instead of creating a branch and making PR
705 -- in "GitHub" workflow, you'd just create a patch and offer it.
706 -- You can do that with `git` too. Push (a branch with) commit to remote
707 -- and ask other to cherry-pick that commit. Yet, in git identity of commit
708 -- changes when it applied to other trees, where patches in pijul have
709 -- will continue to have the same hash.
711 -- Unfortunately pijul doesn't talk about conflict resolution.
712 -- It seems that you get something like:
717 -- Unresolved conflicts:
718 -- (fix conflicts and record the resolution with "pijul record ...")
724 -- >> >>>>>>>>>>>>>>>>>>>>>>>>>>>>>
726 -- ================================
728 -- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
731 -- And then the `pijul dependencies` would draw you a graph like
734 -- -----> foo on branch B ----->
735 -- resolve conflict Initial patch
736 -- -----> foo on branch A ----->
738 -- Which is seems reasonable.
740 -- So currently, pijul support is very experimental, and most likely
741 -- won't work, even the basics are in place. Tests are also written
742 -- but disabled, as the branching model differs from `git` one,
743 -- for which tests are written.
744 vcsPijul
:: VCS Program
747 { vcsRepoType
= KnownRepoType Pijul
748 , vcsProgram
= pijulProgram
755 -- \^ it seems that pijul does not have verbose flag
757 -> SourceRepositoryPackage f
760 -> [ProgramInvocation
]
761 vcsCloneRepo _verbosity prog repo srcuri destdir
=
762 [programInvocation prog cloneArgs
]
763 -- And if there's a tag, we have to do that in a second step:
764 ++ [ (programInvocation prog
(checkoutArgs tag
))
765 { progInvokeCwd
= Just destdir
767 | tag
<- maybeToList (srpTag repo
)
770 cloneArgs
:: [String]
772 ["clone", srcuri
, destdir
]
774 branchArgs
:: [String]
775 branchArgs
= case srpBranch repo
of
776 Just b
-> ["--from-branch", b
]
778 checkoutArgs tag
= "checkout" : [tag
] -- TODO: this probably doesn't work either
782 -> [(SourceRepositoryPackage f
, FilePath)]
783 -> IO [MonitorFilePath
]
784 vcsSyncRepos _ _
[] = return []
788 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
789 vcsSyncRepo verbosity pijulProg primaryRepo primaryLocalDir Nothing
791 [ vcsSyncRepo verbosity pijulProg repo localDir
(Just primaryLocalDir
)
792 |
(repo
, localDir
) <- secondaryRepos
795 [ monitorDirectoryExistence dir
796 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
799 vcsSyncRepo verbosity pijulProg SourceRepositoryPackage
{..} localDir peer
= do
800 exists
<- doesDirectoryExist localDir
802 then pijul localDir
["pull"] -- TODO: this probably doesn't work.
803 else pijul
(takeDirectory localDir
) cloneArgs
804 pijul localDir checkoutArgs
806 pijul
:: FilePath -> [String] -> IO ()
808 runProgramInvocation verbosity
$
809 (programInvocation pijulProg args
)
810 { progInvokeCwd
= Just cwd
813 cloneArgs
:: [String]
815 ["clone", loc
, localDir
]
818 Just peerLocalDir
-> [peerLocalDir
]
821 checkoutArgs
:: [String]
822 checkoutArgs
= "checkout" : ["--force", checkoutTarget
, "--"]
823 checkoutTarget
= fromMaybe "HEAD" (srpBranch `mplus` srpTag
) -- TODO: this is definitely wrong.
825 pijulProgram
:: Program
827 (simpleProgram
"pijul")
828 { programFindVersion
= findProgramVersion
"--version" $ \str
->
831 (_
: ver
: _
) |
all isTypical ver
-> ver
835 isNum c
= c
>= '0' && c
<= '9'
836 isTypical c
= isNum c || c
== '.'