Skip to content

Commit bb83526

Browse files
authored
Merge pull request #5 from unisoncomputing/cp/definition-diffs
Definition Diffs
2 parents 082bfbf + 4f8d3f8 commit bb83526

File tree

25 files changed

+2792
-37
lines changed

25 files changed

+2792
-37
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ jobs:
179179
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
180180
- name: Generate artifact attestation
181181
uses: actions/[email protected]
182-
if: ${{ env.is_published_build }}
182+
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') }}
183183
with:
184184
subject-name: ${{ env.container_registry }}/${{ env.docker_image_name}}
185185
subject-digest: ${{ steps.push.outputs.digest }}

package.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies:
4646
- cookie
4747
- cryptonite
4848
- data-default
49+
- Diff
4950
- either
5051
- extra
5152
- share-utils

share-api.cabal

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ library
182182
ImportQualifiedPost
183183
ghc-options: -Wall -Werror -Wno-name-shadowing -Wno-type-defaults -Wno-missing-pattern-synonym-signatures -fprint-expanded-synonyms -fwrite-ide-info -O2 -funbox-strict-fields
184184
build-depends:
185-
MonadRandom
185+
Diff
186+
, MonadRandom
186187
, aeson
187188
, async
188189
, base >=4.7 && <5
@@ -321,7 +322,8 @@ executable share-api
321322
ImportQualifiedPost
322323
ghc-options: -Wall -Werror -Wno-name-shadowing -Wno-type-defaults -Wno-missing-pattern-synonym-signatures -fprint-expanded-synonyms -fwrite-ide-info -O2 -funbox-strict-fields -threaded -rtsopts "-with-rtsopts=-N -A32m -qn2 -T"
323324
build-depends:
324-
MonadRandom
325+
Diff
326+
, MonadRandom
325327
, aeson
326328
, async
327329
, base >=4.7 && <5

src/Share/Backend.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ module Share.Backend
1717
Backend.bestNameForTerm,
1818
Backend.bestNameForType,
1919
Backend.termsToSyntax,
20+
Backend.termsToSyntaxOf,
2021
Backend.typesToSyntax,
22+
Backend.typesToSyntaxOf,
2123
Backend.definitionResultsDependencies,
2224
Backend.DefinitionResults (..),
2325
Backend.IncludeCycles (..),

src/Share/Postgres/NameLookups/Ops.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import U.Codebase.Referent (ConstructorType, Referent)
3232
import Unison.Codebase.Path (Path)
3333
import Unison.Codebase.Path qualified as Path
3434
import Unison.Debug qualified as Debug
35-
import Unison.HashQualified qualified as HQ
3635
import Unison.Name (Name)
3736
import Unison.Name qualified as Name
3837
import Unison.NameSegment (NameSegment (..))
@@ -91,10 +90,10 @@ namesPerspectiveForRootAndPath rootBranchHashId namespace = do
9190
--
9291
-- A name root is either a project root or a dependency root.
9392
-- E.g. @.myproject.some.namespace -> .myproject@ or @.myproject.lib.base.List -> .myproject.lib.base@
94-
relocateToNameRoot :: PG.QueryM m => Path -> HQ.HashQualified Name -> BranchHashId -> m (NamesPerspective, HQ.HashQualified Name)
93+
relocateToNameRoot :: (PG.QueryM m, Traversable hq) => Path -> hq Name -> BranchHashId -> m (NamesPerspective, hq Name)
9594
relocateToNameRoot perspective query rootBh = do
9695
-- The namespace containing the name path
97-
let nameLocation = case HQ.toName query of
96+
let nameLocation = case getFirst query of
9897
Just name ->
9998
name
10099
& Name.segments

src/Share/Prelude.hs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Share.Prelude
99
readMaybe,
1010
eitherToMaybe,
1111
maybeToEither,
12+
getFirst,
1213
unifyEither,
1314
fromMaybeT,
1415
guardM,
@@ -80,10 +81,10 @@ import Data.Traversable as X
8081
import Data.Typeable (Typeable)
8182
import Data.Void as X
8283
import Data.Word as X (Word64)
83-
import Share.Prelude.Orphans ()
84-
import Share.Utils.Show (tShow)
8584
import GHC.Generics (Generic)
8685
import GHC.Stack (HasCallStack)
86+
import Share.Prelude.Orphans ()
87+
import Share.Utils.Show (tShow)
8788
import Text.Read (readMaybe)
8889
import Unison.Util.Monoid (foldMapM)
8990
import UnliftIO as X (Exception (..), MonadUnliftIO, bracket, bracket_, throwIO, try)
@@ -92,6 +93,9 @@ import Witch.Utility as X (as)
9293
import Witherable as X hiding (filter)
9394
import Prelude as X hiding (log)
9495

96+
getFirst :: (Foldable f) => f a -> Maybe a
97+
getFirst = listToMaybe . toList
98+
9599
-- | Throws an error with the provided message and applicable callstack.
96100
todo :: (HasCallStack) => String -> a
97101
todo = error

src/Share/Web/Share/Projects/API.hs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
module Share.Web.Share.Projects.API where
55

6+
import Servant
67
import Share.IDs
78
import Share.OAuth.Session (MaybeAuthenticatedSession)
89
import Share.Utils.Caching (Cached)
@@ -14,7 +15,7 @@ import Share.Web.Share.Projects.Types
1415
import Share.Web.Share.Releases.API
1516
import Share.Web.Share.Tickets.API (TicketsByProjectAPI)
1617
import Share.Web.Share.Types
17-
import Servant
18+
import Unison.Name (Name)
1819

1920
type ProjectsAPI =
2021
( ListProjectsForUserEndpoint
@@ -27,7 +28,12 @@ type ProjectResourceAPI =
2728
:<|> ("releases" :> ProjectReleasesAPI)
2829
:<|> ("contributions" :> ContributionsByProjectAPI)
2930
:<|> ("tickets" :> TicketsByProjectAPI)
30-
:<|> ("diff" :> "namespaces" :> ProjectDiffNamespaceEndpoint)
31+
:<|> ( "diff"
32+
:> ( "namespaces" :> ProjectDiffNamespacesEndpoint
33+
:<|> "terms" :> ProjectDiffTermsEndpoint
34+
:<|> "types" :> ProjectDiffTypesEndpoint
35+
)
36+
)
3137
:<|> CreateProjectEndpoint
3238
:<|> UpdateProjectEndpoint
3339
:<|> DeleteProjectEndpoint
@@ -37,11 +43,25 @@ type ProjectResourceAPI =
3743
:<|> "maintainers" :> MaintainersResourceAPI
3844
)
3945

40-
type ProjectDiffNamespaceEndpoint =
46+
type ProjectDiffNamespacesEndpoint =
4147
RequiredQueryParam "old" BranchOrReleaseShortHand
4248
:> RequiredQueryParam "new" BranchOrReleaseShortHand
4349
:> Get '[JSON] (Cached JSON ShareNamespaceDiffResponse)
4450

51+
type ProjectDiffTermsEndpoint =
52+
RequiredQueryParam "oldBranchRef" BranchOrReleaseShortHand
53+
:> RequiredQueryParam "newBranchRef" BranchOrReleaseShortHand
54+
:> RequiredQueryParam "oldTerm" Name
55+
:> RequiredQueryParam "newTerm" Name
56+
:> Get '[JSON] ShareTermDiffResponse
57+
58+
type ProjectDiffTypesEndpoint =
59+
RequiredQueryParam "oldBranchRef" BranchOrReleaseShortHand
60+
:> RequiredQueryParam "newBranchRef" BranchOrReleaseShortHand
61+
:> RequiredQueryParam "oldType" Name
62+
:> RequiredQueryParam "newType" Name
63+
:> Get '[JSON] ShareTypeDiffResponse
64+
4565
type CreateProjectEndpoint =
4666
ReqBody '[JSON] CreateProjectRequest
4767
:> Post '[JSON] CreateProjectResponse

src/Share/Web/Share/Projects/Impl.hs

Lines changed: 132 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,25 @@ import Data.ByteString.Lazy qualified as BL
1111
import Data.Map qualified as Map
1212
import Data.Text qualified as Text
1313
import Data.Text.Encoding qualified as Text
14-
import Share.Branch (Branch (..), defaultBranchShorthand)
14+
import Servant
15+
import Share.Branch (defaultBranchShorthand)
16+
import Share.Branch qualified as Branch
17+
import Share.Codebase (CodebaseEnv)
18+
import Share.Codebase qualified as Codebase
1519
import Share.IDs (PrefixedHash (..), ProjectSlug (..), UserHandle, UserId)
1620
import Share.IDs qualified as IDs
1721
import Share.OAuth.Session
1822
import Share.Postgres qualified as PG
1923
import Share.Postgres.Causal.Queries qualified as CausalQ
20-
import Share.Postgres.IDs (CausalId)
24+
import Share.Postgres.IDs (BranchHashId, CausalId)
25+
import Share.Postgres.NameLookups.Ops qualified as NameLookupOps
2126
import Share.Postgres.Ops qualified as PGO
2227
import Share.Postgres.Projects.Queries qualified as ProjectsQ
2328
import Share.Postgres.Queries qualified as Q
2429
import Share.Postgres.Users.Queries qualified as UsersQ
2530
import Share.Prelude
2631
import Share.Project (Project (..))
27-
import Share.Release (Release (..))
32+
import Share.Release qualified as Release
2833
import Share.User (User (..))
2934
import Share.Utils.API ((:++) (..))
3035
import Share.Utils.Caching (Cached)
@@ -43,7 +48,17 @@ import Share.Web.Share.Projects.Types
4348
import Share.Web.Share.Releases.Impl (getProjectReleaseReadmeEndpoint, releasesServer)
4449
import Share.Web.Share.Tickets.Impl (ticketsByProjectServer)
4550
import Share.Web.Share.Types
46-
import Servant
51+
import Unison.Codebase.Path qualified as Path
52+
import Unison.Name (Name)
53+
import Unison.PrettyPrintEnvDecl qualified as PPED
54+
import Unison.PrettyPrintEnvDecl.Postgres qualified as PPEPostgres
55+
import Unison.Server.Backend.DefinitionDiff qualified as DefinitionDiff
56+
import Unison.Server.NameSearch.Postgres qualified as PGNameSearch
57+
import Unison.Server.Orphans ()
58+
import Unison.Server.Share.Definitions qualified as Definitions
59+
import Unison.Server.Types (TermDefinition (..), TypeDefinition (..))
60+
import Unison.Syntax.Name qualified as Name
61+
import Unison.Util.Pretty (Width)
4762

4863
data ProjectErrors
4964
= MaintainersAlreadyExist [UserId]
@@ -91,7 +106,10 @@ projectServer session handle =
91106
:<|> releasesServer session handle slug
92107
:<|> contributionsByProjectServer session handle slug
93108
:<|> ticketsByProjectServer session handle slug
94-
:<|> diffNamespacesEndpoint session handle slug
109+
:<|> ( diffNamespacesEndpoint session handle slug
110+
:<|> diffTermsEndpoint session handle slug
111+
:<|> diffTypesEndpoint session handle slug
112+
)
95113
:<|> createProjectEndpoint session handle slug
96114
:<|> updateProjectEndpoint session handle slug
97115
:<|> deleteProjectEndpoint session handle slug
@@ -142,8 +160,8 @@ diffNamespacesEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle project
142160
project@Project {projectId} <- PG.runTransactionOrRespondError do
143161
Q.projectByShortHand projectShortHand `whenNothingM` throwError (EntityMissing (ErrorID "project-not-found") ("Project not found: " <> IDs.toText @IDs.ProjectShortHand projectShortHand))
144162
authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkProjectBranchDiff callerUserId project
145-
oldCausalId <- resolveBranchOrRelease projectId oldShortHand
146-
newCausalId <- resolveBranchOrRelease projectId newShortHand
163+
(_, oldCausalId, _oldBranchId) <- namespaceHashForBranchOrRelease authZReceipt project oldShortHand
164+
(_, newCausalId, _newBranchId) <- namespaceHashForBranchOrRelease authZReceipt project newShortHand
147165

148166
let cacheKeys = [IDs.toText projectId, IDs.toText oldShortHand, IDs.toText newShortHand, Caching.causalIdCacheKey oldCausalId, Caching.causalIdCacheKey newCausalId]
149167
Caching.cachedResponse authZReceipt "project-diff-namespaces" cacheKeys do
@@ -163,16 +181,113 @@ diffNamespacesEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle project
163181
}
164182
where
165183
projectShortHand = IDs.ProjectShortHand {userHandle, projectSlug}
166-
resolveBranchOrRelease :: IDs.ProjectId -> IDs.BranchOrReleaseShortHand -> WebApp CausalId
167-
resolveBranchOrRelease projectId = \case
168-
IDs.IsBranchShortHand branchShortHand -> do
169-
PG.runTransactionOrRespondError $ do
170-
Branch {causal = branchHead} <- Q.branchByProjectIdAndShortHand projectId branchShortHand `whenNothingM` throwError (EntityMissing (ErrorID "branch-not-found") ("Branch not found: " <> IDs.toText @IDs.BranchShortHand branchShortHand))
171-
pure branchHead
172-
IDs.IsReleaseShortHand releaseShortHand -> do
173-
PG.runTransactionOrRespondError $ do
174-
Release {squashedCausal = releaseHead} <- Q.releaseByProjectIdAndReleaseShortHand projectId releaseShortHand `whenNothingM` throwError (EntityMissing (ErrorID "release-not-found") ("Release not found: " <> IDs.toText @IDs.ReleaseShortHand releaseShortHand))
175-
pure releaseHead
184+
185+
diffTermsEndpoint ::
186+
Maybe Session ->
187+
UserHandle ->
188+
ProjectSlug ->
189+
IDs.BranchOrReleaseShortHand ->
190+
IDs.BranchOrReleaseShortHand ->
191+
Name ->
192+
Name ->
193+
WebApp ShareTermDiffResponse
194+
diffTermsEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle projectSlug oldShortHand newShortHand oldTermName newTermName =
195+
do
196+
project <- PG.runTransactionOrRespondError do
197+
Q.projectByShortHand projectShortHand `whenNothingM` throwError (EntityMissing (ErrorID "project-not-found") ("Project not found: " <> IDs.toText @IDs.ProjectShortHand projectShortHand))
198+
authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkProjectBranchDiff callerUserId project
199+
oldTerm@(TermDefinition {termDefinition = oldDisplayObj}) <- getTermDefinition authZReceipt project oldShortHand oldTermName `whenNothingM` respondError (EntityMissing (ErrorID "term-not-found") ("Term not found: " <> IDs.toText oldShortHand <> ":" <> Name.toText oldTermName))
200+
newTerm@(TermDefinition {termDefinition = newDisplayObj}) <- getTermDefinition authZReceipt project newShortHand newTermName `whenNothingM` respondError (EntityMissing (ErrorID "term-not-found") ("Term not found: " <> IDs.toText newShortHand <> ":" <> Name.toText newTermName))
201+
let termDiffDisplayObject = DefinitionDiff.diffDisplayObjects oldDisplayObj newDisplayObj
202+
pure $
203+
ShareTermDiffResponse
204+
{ project = projectShortHand,
205+
oldBranch = oldShortHand,
206+
newBranch = newShortHand,
207+
oldTerm = oldTerm,
208+
newTerm = newTerm,
209+
diff = termDiffDisplayObject
210+
}
211+
where
212+
renderWidth :: Width
213+
renderWidth = 80
214+
getTermDefinition :: AuthZ.AuthZReceipt -> Project -> IDs.BranchOrReleaseShortHand -> Name -> WebApp (Maybe TermDefinition)
215+
getTermDefinition authZReceipt project shorthand name = do
216+
(codebase, _causalId, bhId) <- namespaceHashForBranchOrRelease authZReceipt project shorthand
217+
let perspective = Path.empty
218+
(namesPerspective, Identity relocatedName) <- PG.runTransaction $ NameLookupOps.relocateToNameRoot perspective (Identity name) bhId
219+
let ppedBuilder deps = (PPED.biasTo [name]) <$> lift (PPEPostgres.ppedForReferences namesPerspective deps)
220+
let nameSearch = PGNameSearch.nameSearchForPerspective namesPerspective
221+
rt <- Codebase.codebaseRuntime codebase
222+
Codebase.runCodebaseTransaction codebase do
223+
Definitions.termDefinitionByName ppedBuilder nameSearch renderWidth rt relocatedName
224+
225+
projectShortHand :: IDs.ProjectShortHand
226+
projectShortHand = IDs.ProjectShortHand {userHandle, projectSlug}
227+
228+
diffTypesEndpoint ::
229+
Maybe Session ->
230+
UserHandle ->
231+
ProjectSlug ->
232+
IDs.BranchOrReleaseShortHand ->
233+
IDs.BranchOrReleaseShortHand ->
234+
Name ->
235+
Name ->
236+
WebApp ShareTypeDiffResponse
237+
diffTypesEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle projectSlug oldShortHand newShortHand oldTypeName newTypeName =
238+
do
239+
project <- PG.runTransactionOrRespondError do
240+
Q.projectByShortHand projectShortHand `whenNothingM` throwError (EntityMissing (ErrorID "project-not-found") ("Project not found: " <> IDs.toText @IDs.ProjectShortHand projectShortHand))
241+
authZReceipt <- AuthZ.permissionGuard $ AuthZ.checkProjectBranchDiff callerUserId project
242+
sourceType@(TypeDefinition {typeDefinition = sourceDisplayObj}) <- getTypeDefinition authZReceipt project oldShortHand oldTypeName `whenNothingM` respondError (EntityMissing (ErrorID "type-not-found") ("Type not found: " <> IDs.toText oldShortHand <> ":" <> Name.toText oldTypeName))
243+
newType@(TypeDefinition {typeDefinition = newDisplayObj}) <- getTypeDefinition authZReceipt project newShortHand newTypeName `whenNothingM` respondError (EntityMissing (ErrorID "type-not-found") ("Type not found: " <> IDs.toText newShortHand <> ":" <> Name.toText newTypeName))
244+
let typeDiffDisplayObject = DefinitionDiff.diffDisplayObjects sourceDisplayObj newDisplayObj
245+
pure $
246+
ShareTypeDiffResponse
247+
{ project = projectShortHand,
248+
oldBranch = oldShortHand,
249+
newBranch = newShortHand,
250+
oldType = sourceType,
251+
newType = newType,
252+
diff = typeDiffDisplayObject
253+
}
254+
where
255+
renderWidth :: Width
256+
renderWidth = 80
257+
getTypeDefinition :: AuthZ.AuthZReceipt -> Project -> IDs.BranchOrReleaseShortHand -> Name -> WebApp (Maybe TypeDefinition)
258+
getTypeDefinition authZReceipt project shorthand name = do
259+
(codebase, _causalId, bhId) <- namespaceHashForBranchOrRelease authZReceipt project shorthand
260+
let perspective = Path.empty
261+
(namesPerspective, Identity relocatedName) <- PG.runTransaction $ NameLookupOps.relocateToNameRoot perspective (Identity name) bhId
262+
let ppedBuilder deps = (PPED.biasTo [name]) <$> lift (PPEPostgres.ppedForReferences namesPerspective deps)
263+
let nameSearch = PGNameSearch.nameSearchForPerspective namesPerspective
264+
rt <- Codebase.codebaseRuntime codebase
265+
Codebase.runCodebaseTransaction codebase do
266+
Definitions.typeDefinitionByName ppedBuilder nameSearch renderWidth rt relocatedName
267+
268+
projectShortHand :: IDs.ProjectShortHand
269+
projectShortHand = IDs.ProjectShortHand {userHandle, projectSlug}
270+
271+
namespaceHashForBranchOrRelease :: AuthZ.AuthZReceipt -> Project -> IDs.BranchOrReleaseShortHand -> WebApp (CodebaseEnv, CausalId, BranchHashId)
272+
namespaceHashForBranchOrRelease authZReceipt Project {projectId, ownerUserId = projectOwnerUserId} = \case
273+
IDs.IsBranchShortHand branchShortHand -> do
274+
PG.runTransactionOrRespondError $ do
275+
branch <- Q.branchByProjectIdAndShortHand projectId branchShortHand `whenNothingM` throwError (EntityMissing (ErrorID "branch-not-found") ("Branch not found: " <> IDs.toText @IDs.BranchShortHand branchShortHand))
276+
let causalId = Branch.causal branch
277+
let codebaseLoc = Codebase.codebaseLocationForProjectBranchCodebase projectOwnerUserId (Branch.contributorId branch)
278+
let codebase = Codebase.codebaseEnv authZReceipt codebaseLoc
279+
Codebase.codebaseMToTransaction codebase do
280+
branchHashId <- CausalQ.expectNamespaceIdForCausal causalId
281+
pure (codebase, causalId, branchHashId)
282+
IDs.IsReleaseShortHand releaseShortHand -> do
283+
PG.runTransactionOrRespondError $ do
284+
release <- Q.releaseByProjectIdAndReleaseShortHand projectId releaseShortHand `whenNothingM` throwError (EntityMissing (ErrorID "release-not-found") ("Release not found: " <> IDs.toText @IDs.ReleaseShortHand releaseShortHand))
285+
let causalId = Release.squashedCausal release
286+
let codebaseLoc = Codebase.codebaseLocationForProjectRelease projectOwnerUserId
287+
let codebase = Codebase.codebaseEnv authZReceipt codebaseLoc
288+
Codebase.codebaseMToTransaction codebase do
289+
branchHashId <- CausalQ.expectNamespaceIdForCausal causalId
290+
pure (codebase, causalId, branchHashId)
176291

177292
createProjectEndpoint :: Maybe Session -> UserHandle -> ProjectSlug -> CreateProjectRequest -> WebApp CreateProjectResponse
178293
createProjectEndpoint (AuthN.MaybeAuthedUserID callerUserId) userHandle projectSlug req = do

0 commit comments

Comments
 (0)